├── .dockerignore ├── .gitignore ├── internal ├── infrastructure │ ├── config │ │ ├── special_ips.go │ │ ├── proxy_categories.go │ │ ├── file_output_extensions.go │ │ ├── testing_sites.go │ │ └── private_ips.go │ └── repository │ │ ├── source_repository.go │ │ ├── source_repository_test.go │ │ ├── proxy_repository.go │ │ ├── repository_test.go │ │ ├── file_repository.go │ │ ├── file_repository_test.go │ │ └── proxy_repository_test.go ├── entity │ ├── proxy_xml.go │ ├── source.go │ ├── proxy.go │ └── source_test.go ├── usecase │ ├── source_usecase.go │ ├── file_usecase.go │ ├── file_usecase_test.go │ ├── proxy_usecase.go │ ├── source_usecase_test.go │ ├── proxy_usecase_test.go │ └── usecase_test.go └── service │ ├── proxy_service.go │ └── proxy_service_test.go ├── go.mod ├── deployments ├── Dockerfile └── goreleaser.yml ├── pkg └── utils │ ├── url_parser_util.go │ ├── csv_writer_util.go │ ├── url_parser_util_test.go │ ├── util_test.go │ ├── fetcher_util.go │ ├── csv_writer_util_test.go │ └── fetcher_util_test.go ├── env.example ├── .github └── workflows │ ├── static-analysis.yml │ ├── release.yml │ └── continuous-integration.yml ├── go.sum ├── LICENSE ├── cmd └── main.go ├── docs └── README.template.md └── README.md /.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | vendor 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | vendor 3 | fresh-proxy-list 4 | coverage.html 5 | coverage.out 6 | *temp.* -------------------------------------------------------------------------------- /internal/infrastructure/config/special_ips.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | var SpecialIPs = []string{ 4 | "0.0.0.0", 5 | "127.0.0.1", 6 | "255.255.255.255", 7 | } 8 | -------------------------------------------------------------------------------- /internal/infrastructure/config/proxy_categories.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | var ProxyCategories = []string{ 4 | "HTTP", 5 | "HTTPS", 6 | "SOCKS4", 7 | "SOCKS5", 8 | } 9 | -------------------------------------------------------------------------------- /internal/infrastructure/config/file_output_extensions.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | var FileOutputExtensions = []string{ 4 | "csv", 5 | "json", 6 | "xml", 7 | "yaml", 8 | } 9 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/fyvri/fresh-proxy-list 2 | 3 | go 1.23 4 | 5 | require ( 6 | github.com/joho/godotenv v1.5.1 7 | gopkg.in/yaml.v3 v3.0.1 8 | h12.io/socks v1.0.3 9 | ) 10 | -------------------------------------------------------------------------------- /deployments/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:latest 2 | 3 | # Set the working directory 4 | WORKDIR /app 5 | 6 | # Copy compiled binary into the container 7 | COPY fresh-proxy-list /app/fresh-proxy-list 8 | 9 | # Set default command to run application 10 | ENTRYPOINT ["/app/fresh-proxy-list"] 11 | -------------------------------------------------------------------------------- /pkg/utils/url_parser_util.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "net/url" 5 | ) 6 | 7 | type URLParserUtil struct{} 8 | 9 | type URLParserUtilInterface interface { 10 | Parse(rawURL string) (*url.URL, error) 11 | } 12 | 13 | func NewURLParser() URLParserUtilInterface { 14 | return &URLParserUtil{} 15 | } 16 | 17 | func (u *URLParserUtil) Parse(rawURL string) (*url.URL, error) { 18 | return url.Parse(rawURL) 19 | } 20 | -------------------------------------------------------------------------------- /internal/entity/proxy_xml.go: -------------------------------------------------------------------------------- 1 | package entity 2 | 3 | import "encoding/xml" 4 | 5 | type ProxyXMLClassicView struct { 6 | XMLName xml.Name `xml:"Proxies"` 7 | Proxies []string `xml:"Proxy"` 8 | } 9 | 10 | type ProxyXMLAdvancedView struct { 11 | XMLName xml.Name `xml:"Proxies"` 12 | Proxies []Proxy `xml:"Proxy"` 13 | } 14 | 15 | type ProxyXMLAllAdvancedView struct { 16 | XMLName xml.Name `xml:"Proxies"` 17 | Proxies []AdvancedProxy `xml:"Proxy"` 18 | } 19 | -------------------------------------------------------------------------------- /internal/infrastructure/config/testing_sites.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | var HTTPTestingSites = []string{ 4 | "http://ifconfig.me/ip", 5 | "http://api.ipaddress.com/myip", 6 | "http://checkip.amazonaws.com", 7 | } 8 | 9 | var HTTPSTestingSites = []string{ 10 | "https://ifconfig.me/ip", 11 | "https://api.ipaddress.com/myip", 12 | "https://checkip.amazonaws.com", 13 | "https://google.com", 14 | "https://bing.com", 15 | "https://yahoo.com", 16 | "https://api.ipify.org", 17 | "https://ipinfo.io/ip", 18 | } 19 | -------------------------------------------------------------------------------- /env.example: -------------------------------------------------------------------------------- 1 | PROXY_RESOURCES=[{"method":"LIST","category":"HTTP","url":"","is_checked":true},{"method":"LIST","category":"HTTPS","url":"","is_checked":true},{"method":"LIST","category":"SOCKS4","url":"","is_checked":true},{"method":"LIST","category":"SOCKS5","url":"","is_checked":true},{"method":"SCRAP","category":"HTTP","url":"","is_checked":true},{"method":"SCRAP","category":"HTTPS","url":"","is_checked":true},{"method":"SCRAP","category":"SOCKS4","url":"","is_checked":true},{"method":"SCRAP","category":"SOCKS5","url":"","is_checked":true}] -------------------------------------------------------------------------------- /.github/workflows/static-analysis.yml: -------------------------------------------------------------------------------- 1 | name: Static Analysis 2 | 3 | on: 4 | push: 5 | pull_request: 6 | 7 | jobs: 8 | build: 9 | name: Build and Testing 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - name: Checkout code 14 | uses: actions/checkout@v4 15 | 16 | - name: Set up Go 17 | uses: actions/setup-go@v5 18 | with: 19 | go-version-file: "go.mod" 20 | 21 | - name: Build 22 | run: go build -v ./... 23 | 24 | - name: Test 25 | run: go test -v ./... 26 | -------------------------------------------------------------------------------- /internal/entity/source.go: -------------------------------------------------------------------------------- 1 | package entity 2 | 3 | import "encoding/json" 4 | 5 | type Source struct { 6 | Method string `json:"method"` 7 | Category string `json:"category"` 8 | URL string `json:"url"` 9 | IsChecked bool `json:"is_checked"` 10 | } 11 | 12 | func (s *Source) UnmarshalJSON(data []byte) error { 13 | type Alias Source 14 | alias := &struct { 15 | IsChecked *bool `json:"is_checked"` 16 | *Alias 17 | }{ 18 | Alias: (*Alias)(s), 19 | } 20 | 21 | if err := json.Unmarshal(data, &alias); err != nil { 22 | return err 23 | } 24 | 25 | if alias.IsChecked == nil { 26 | s.IsChecked = true 27 | } else { 28 | s.IsChecked = *alias.IsChecked 29 | } 30 | 31 | return nil 32 | } 33 | -------------------------------------------------------------------------------- /pkg/utils/csv_writer_util.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "encoding/csv" 5 | "io" 6 | ) 7 | 8 | type CSVWriterUtil struct { 9 | } 10 | 11 | type CSVWriterUtilInterface interface { 12 | Init(w io.Writer) *csv.Writer 13 | Flush(csvWriter *csv.Writer) 14 | Write(csvWriter *csv.Writer, record []string) error 15 | } 16 | 17 | func NewCSVWriter() CSVWriterUtilInterface { 18 | return &CSVWriterUtil{} 19 | } 20 | 21 | func (u *CSVWriterUtil) Init(w io.Writer) *csv.Writer { 22 | return csv.NewWriter(w) 23 | } 24 | 25 | func (u *CSVWriterUtil) Flush(csvWriter *csv.Writer) { 26 | csvWriter.Flush() 27 | } 28 | 29 | func (u *CSVWriterUtil) Write(csvWriter *csv.Writer, record []string) error { 30 | return csvWriter.Write(record) 31 | } 32 | -------------------------------------------------------------------------------- /internal/entity/proxy.go: -------------------------------------------------------------------------------- 1 | package entity 2 | 3 | type Proxy struct { 4 | Category string `json:"category" yaml:"category"` 5 | Proxy string `json:"proxy" yaml:"proxy"` 6 | IP string `json:"ip" yaml:"ip"` 7 | Port string `json:"port" yaml:"port"` 8 | TimeTaken float64 `json:"time_taken" yaml:"time_taken"` 9 | CheckedAt string `json:"checked_at" yaml:"checked_at"` 10 | } 11 | 12 | type AdvancedProxy struct { 13 | Proxy string `json:"proxy" yaml:"proxy"` 14 | IP string `json:"ip" yaml:"ip"` 15 | Port string `json:"port" yaml:"port"` 16 | TimeTaken float64 `json:"time_taken" yaml:"time_taken"` 17 | CheckedAt string `json:"checked_at" yaml:"checked_at"` 18 | Categories []string `json:"categories" yaml:"categories"` 19 | } 20 | -------------------------------------------------------------------------------- /internal/infrastructure/config/private_ips.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "net" 5 | ) 6 | 7 | var PrivateIPs = []net.IPNet{ 8 | {IP: net.IP{10, 0, 0, 0}, Mask: net.CIDRMask(8, 32)}, // Private range A 9 | {IP: net.IP{172, 16, 0, 0}, Mask: net.CIDRMask(12, 32)}, // Private range B 10 | {IP: net.IP{192, 168, 0, 0}, Mask: net.CIDRMask(16, 32)}, // Private range C 11 | {IP: net.IP{169, 254, 0, 0}, Mask: net.CIDRMask(16, 32)}, // Link-local addresses 12 | {IP: net.IP{224, 0, 0, 0}, Mask: net.CIDRMask(4, 32)}, // Multicast addresses 13 | {IP: net.IP{240, 0, 0, 0}, Mask: net.CIDRMask(4, 32)}, // Reserved addresses 14 | {IP: net.IP{127, 0, 0, 0}, Mask: net.CIDRMask(8, 32)}, // Loopback addresses 15 | {IP: net.IP{192, 0, 2, 0}, Mask: net.CIDRMask(24, 32)}, // Documentation Network 16 | } 17 | -------------------------------------------------------------------------------- /internal/infrastructure/repository/source_repository.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | 8 | "github.com/fyvri/fresh-proxy-list/internal/entity" 9 | ) 10 | 11 | type SourceRepository struct { 12 | ProxyResources string 13 | } 14 | 15 | type SourceRepositoryInterface interface { 16 | LoadSources() ([]entity.Source, error) 17 | } 18 | 19 | func NewSourceRepository(proxyResources string) SourceRepositoryInterface { 20 | return &SourceRepository{ 21 | ProxyResources: proxyResources, 22 | } 23 | } 24 | 25 | func (r *SourceRepository) LoadSources() ([]entity.Source, error) { 26 | sourcesJSON := r.ProxyResources 27 | if sourcesJSON == "" { 28 | return nil, errors.New("PROXY_RESOURCES not found on environment") 29 | } 30 | 31 | var sources []entity.Source 32 | err := json.Unmarshal([]byte(sourcesJSON), &sources) 33 | if err != nil { 34 | return nil, fmt.Errorf("error parsing JSON: %v", err) 35 | } 36 | 37 | return sources, nil 38 | } 39 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/h12w/go-socks5 v0.0.0-20200522160539-76189e178364 h1:5XxdakFhqd9dnXoAZy1Mb2R/DZ6D1e+0bGC/JhucGYI= 2 | github.com/h12w/go-socks5 v0.0.0-20200522160539-76189e178364/go.mod h1:eDJQioIyy4Yn3MVivT7rv/39gAJTrA7lgmYr8EW950c= 3 | github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= 4 | github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= 5 | github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2 h1:JhzVVoYvbOACxoUmOs6V/G4D5nPVUW73rKvXxP4XUJc= 6 | github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2/go.mod h1:iIss55rKnNBTvrwdmkUpLnDpZoAHvWaiq5+iMmen4AE= 7 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 8 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 9 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 10 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 11 | h12.io/socks v1.0.3 h1:Ka3qaQewws4j4/eDQnOdpr4wXsC//dXtWvftlIcCQUo= 12 | h12.io/socks v1.0.3/go.mod h1:AIhxy1jOId/XCz9BO+EIgNL2rQiPTBNnOfnVnQ+3Eck= 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Azis Alvriyanto 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /pkg/utils/url_parser_util_test.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "net/url" 5 | "reflect" 6 | "testing" 7 | ) 8 | 9 | func TestParse(t *testing.T) { 10 | type args struct { 11 | rawURL string 12 | } 13 | tests := []struct { 14 | name string 15 | args args 16 | want *url.URL 17 | wantError error 18 | }{ 19 | { 20 | name: "ValidURL", 21 | args: args{ 22 | rawURL: testRawURL, 23 | }, 24 | want: &url.URL{ 25 | Scheme: testScheme, 26 | Host: testHost, 27 | }, 28 | wantError: nil, 29 | }, 30 | { 31 | name: "ValidURLWithFullURL", 32 | args: args{ 33 | rawURL: testFullURL, 34 | }, 35 | want: &url.URL{ 36 | Scheme: testScheme, 37 | Host: testHost, 38 | Path: testPath, 39 | RawQuery: testRawQuery, 40 | }, 41 | wantError: nil, 42 | }, 43 | } 44 | 45 | for _, tt := range tests { 46 | t.Run(tt.name, func(t *testing.T) { 47 | u := NewURLParser() 48 | got, err := u.Parse(tt.args.rawURL) 49 | 50 | if tt.wantError != nil { 51 | t.Errorf(expectedErrorButGotMessage, "Parse()", err, tt.wantError) 52 | } 53 | 54 | if !reflect.DeepEqual(got, tt.want) { 55 | t.Errorf(expectedErrorButGotMessage, "Parse()", err, tt.wantError) 56 | } 57 | }) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /pkg/utils/util_test.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "io" 5 | "net/http" 6 | ) 7 | 8 | var ( 9 | expectedButGotMessage = "Expected %v = %v, but got = %v" 10 | expectedErrorButGotMessage = "Expected %v error = %v, but got = %v" 11 | expectedReturnNonNil = "Expected %v to return a non-nil %v" 12 | expectedTypeAssertionErrorMessage = "Expected type assertion error, but got = %v" 13 | testScheme = "https" 14 | testHost = "example.com" 15 | testPath = "/path" 16 | testRawQuery = "query=1" 17 | testRawURL = testScheme + "://" + testHost 18 | testFullURL = testRawURL + testPath + "?" + testRawQuery 19 | ) 20 | 21 | type mockTransport struct { 22 | response *http.Response 23 | err error 24 | } 25 | 26 | func (m *mockTransport) RoundTrip(req *http.Request) (*http.Response, error) { 27 | return m.response, m.err 28 | } 29 | 30 | type mockReadCloser struct { 31 | data []byte 32 | errRead error 33 | errClose error 34 | } 35 | 36 | func (m *mockReadCloser) Read(p []byte) (int, error) { 37 | if m.errRead != nil { 38 | return 0, m.errRead 39 | } 40 | copy(p, m.data) 41 | return len(m.data), io.EOF 42 | } 43 | 44 | func (m *mockReadCloser) Close() error { 45 | if m.errClose != nil { 46 | return m.errClose 47 | } 48 | return nil 49 | } 50 | -------------------------------------------------------------------------------- /internal/usecase/source_usecase.go: -------------------------------------------------------------------------------- 1 | package usecase 2 | 3 | import ( 4 | "fmt" 5 | "regexp" 6 | "strings" 7 | 8 | "github.com/fyvri/fresh-proxy-list/internal/entity" 9 | "github.com/fyvri/fresh-proxy-list/internal/infrastructure/repository" 10 | "github.com/fyvri/fresh-proxy-list/pkg/utils" 11 | ) 12 | 13 | type SourceUsecase struct { 14 | SourceRepository repository.SourceRepositoryInterface 15 | FetcherUtil utils.FetcherUtilInterface 16 | } 17 | 18 | type SourceUsecaseInterface interface { 19 | LoadSources() ([]entity.Source, error) 20 | ProcessSource(source *entity.Source) ([]string, error) 21 | } 22 | 23 | func NewSourceUsecase(sourceRepository repository.SourceRepositoryInterface, fetcherUtil utils.FetcherUtilInterface) SourceUsecaseInterface { 24 | return &SourceUsecase{ 25 | SourceRepository: sourceRepository, 26 | FetcherUtil: fetcherUtil, 27 | } 28 | } 29 | 30 | func (uc *SourceUsecase) LoadSources() ([]entity.Source, error) { 31 | return uc.SourceRepository.LoadSources() 32 | } 33 | 34 | func (uc *SourceUsecase) ProcessSource(source *entity.Source) ([]string, error) { 35 | body, err := uc.FetcherUtil.FetchData(source.URL) 36 | if err != nil { 37 | return nil, err 38 | } 39 | 40 | var proxies []string 41 | switch source.Method { 42 | case "LIST": 43 | proxies = strings.Split(strings.TrimSpace(string(body)), "\n") 44 | case "SCRAP": 45 | re := regexp.MustCompile(`[0-9]+(?:\.[0-9]+){3}:[0-9]+`) 46 | proxies = re.FindAllString(string(body), -1) 47 | default: 48 | return nil, fmt.Errorf("source method not found: %s", source.Method) 49 | } 50 | 51 | return proxies, nil 52 | } 53 | -------------------------------------------------------------------------------- /pkg/utils/fetcher_util.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "net/http" 7 | ) 8 | 9 | type FetcherUtil struct { 10 | Client *http.Client 11 | NewRequestFunc func(method string, url string, body io.Reader) (*http.Request, error) 12 | } 13 | 14 | type FetcherUtilInterface interface { 15 | NewRequest(method, url string, body io.Reader) (*http.Request, error) 16 | Do(client *http.Client, req *http.Request) (*http.Response, error) 17 | FetchData(url string) ([]byte, error) 18 | } 19 | 20 | func NewFetcher(client *http.Client, newRequestFunc func(method, url string, body io.Reader) (*http.Request, error)) FetcherUtilInterface { 21 | return &FetcherUtil{ 22 | Client: client, 23 | NewRequestFunc: newRequestFunc, 24 | } 25 | } 26 | 27 | func (u *FetcherUtil) Do(client *http.Client, req *http.Request) (*http.Response, error) { 28 | return client.Do(req) 29 | } 30 | 31 | func (u *FetcherUtil) NewRequest(method string, url string, body io.Reader) (*http.Request, error) { 32 | return http.NewRequest(method, url, body) 33 | } 34 | 35 | func (u *FetcherUtil) FetchData(url string) ([]byte, error) { 36 | req, err := u.NewRequestFunc("GET", url, nil) 37 | if err != nil { 38 | return nil, err 39 | } 40 | 41 | resp, err := u.Do(u.Client, req) 42 | if err != nil { 43 | return nil, err 44 | } 45 | defer resp.Body.Close() 46 | 47 | if resp.StatusCode != http.StatusOK { 48 | body, _ := io.ReadAll(resp.Body) 49 | return body, fmt.Errorf("failed to fetch data: %s", http.StatusText(resp.StatusCode)) 50 | } 51 | 52 | body, err := io.ReadAll(resp.Body) 53 | if err != nil { 54 | return nil, err 55 | } 56 | 57 | return body, nil 58 | } 59 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release Version 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v*" 7 | 8 | permissions: write-all 9 | 10 | jobs: 11 | build: 12 | name: Build and Testing 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout code 16 | uses: actions/checkout@v4 17 | 18 | - name: Set up Go 19 | uses: actions/setup-go@v5 20 | with: 21 | go-version-file: "go.mod" 22 | 23 | - name: Build 24 | run: go build -v ./... 25 | 26 | - name: Test 27 | run: go test -v ./... 28 | 29 | release: 30 | name: Release 31 | runs-on: ubuntu-latest 32 | needs: [build] 33 | steps: 34 | - name: Checkout code 35 | uses: actions/checkout@v4 36 | with: 37 | fetch-depth: 0 38 | 39 | - name: Login to GitHub Container Registry 40 | uses: docker/login-action@v3 41 | with: 42 | registry: ghcr.io 43 | username: ${{ vars.DOCKER_USERNAME }} 44 | password: ${{ secrets.GITHUB_TOKEN }} 45 | 46 | - name: Init tag version name 47 | run: | 48 | echo "TAG_VERSION=${GITHUB_REF_NAME#v}" >> $GITHUB_ENV 49 | 50 | - name: Release 51 | uses: goreleaser/goreleaser-action@v6 52 | with: 53 | distribution: goreleaser 54 | version: "~> v2" 55 | args: release --clean --config ./deployments/goreleaser.yml 56 | env: 57 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 58 | DOCKER_USERNAME: ${{ vars.DOCKER_USERNAME }} 59 | DOCKER_REPOSITORY: ${{ vars.DOCKER_REPOSITORY }} 60 | TAG_VERSION: ${{ env.TAG_VERSION }} 61 | -------------------------------------------------------------------------------- /internal/usecase/file_usecase.go: -------------------------------------------------------------------------------- 1 | package usecase 2 | 3 | import ( 4 | "path/filepath" 5 | "strings" 6 | "sync" 7 | 8 | "github.com/fyvri/fresh-proxy-list/internal/infrastructure/repository" 9 | ) 10 | 11 | type fileUsecase struct { 12 | FileRepository repository.FileRepositoryInterface 13 | ProxyRepository repository.ProxyRepositoryInterface 14 | FileOutputExtensions []string 15 | WaitGroup sync.WaitGroup 16 | } 17 | 18 | type FileUsecaseInterface interface { 19 | SaveFiles() 20 | } 21 | 22 | func NewFileUsecase(fileRepository repository.FileRepositoryInterface, proxyRepository repository.ProxyRepositoryInterface, fileOutputExtensions []string) FileUsecaseInterface { 23 | return &fileUsecase{ 24 | FileRepository: fileRepository, 25 | ProxyRepository: proxyRepository, 26 | FileOutputExtensions: fileOutputExtensions, 27 | WaitGroup: sync.WaitGroup{}, 28 | } 29 | } 30 | 31 | func (uc *fileUsecase) SaveFiles() { 32 | createFile := func(filename string, classic []string, advanced interface{}) { 33 | uc.WaitGroup.Add((len(uc.FileOutputExtensions) * 2) + 1) 34 | 35 | filename = strings.ToLower(filename) 36 | for _, ext := range uc.FileOutputExtensions { 37 | go func(ext string) { 38 | defer uc.WaitGroup.Done() 39 | uc.FileRepository.SaveFile(filepath.Join("storage", "classic", filename+"."+ext), classic, ext) 40 | }(ext) 41 | go func(ext string) { 42 | defer uc.WaitGroup.Done() 43 | uc.FileRepository.SaveFile(filepath.Join("storage", "advanced", filename+"."+ext), advanced, ext) 44 | }(ext) 45 | } 46 | 47 | go func() { 48 | defer uc.WaitGroup.Done() 49 | uc.FileRepository.SaveFile(filepath.Join("storage", "classic", filename+".txt"), classic, "txt") 50 | }() 51 | } 52 | 53 | createFile("all", uc.ProxyRepository.GetAllClassicView(), uc.ProxyRepository.GetAllAdvancedView()) 54 | createFile("http", uc.ProxyRepository.GetHTTPClassicView(), uc.ProxyRepository.GetHTTPAdvancedView()) 55 | createFile("https", uc.ProxyRepository.GetHTTPSClassicView(), uc.ProxyRepository.GetHTTPSAdvancedView()) 56 | createFile("socks4", uc.ProxyRepository.GetSOCKS4ClassicView(), uc.ProxyRepository.GetSOCKS4AdvancedView()) 57 | createFile("socks5", uc.ProxyRepository.GetSOCKS5ClassicView(), uc.ProxyRepository.GetSOCKS5AdvancedView()) 58 | uc.WaitGroup.Wait() 59 | } 60 | -------------------------------------------------------------------------------- /deployments/goreleaser.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | project_name: fresh-proxy-list 3 | env: 4 | - GO111MODULE=on 5 | 6 | builds: 7 | - env: 8 | - CGO_ENABLED=0 9 | 10 | main: ./cmd/main.go 11 | flags: -trimpath 12 | ldflags: 13 | - -s -w 14 | - -extldflags=-static 15 | mod_timestamp: "{{ .CommitTimestamp }}" 16 | goos: 17 | - linux 18 | - windows 19 | - darwin 20 | goarch: 21 | - arm 22 | - arm64 23 | - amd64 24 | 25 | checksum: 26 | name_template: "{{ .ProjectName }}_checksums.txt" 27 | 28 | archives: 29 | - name_template: "{{ .ProjectName }}-{{ .Env.TAG_VERSION }}-{{ .Os }}-{{ .Arch }}" 30 | format_overrides: 31 | - goos: windows 32 | format: zip 33 | files: 34 | - README*.md 35 | - LICENSE 36 | 37 | dockers: 38 | - image_templates: 39 | - "ghcr.io/{{ .Env.DOCKER_USERNAME }}/{{ .Env.DOCKER_REPOSITORY }}:{{ .Env.TAG_VERSION }}-amd64" 40 | use: buildx 41 | dockerfile: ./deployments/Dockerfile 42 | build_flag_templates: 43 | - "--platform=linux/amd64" 44 | - image_templates: 45 | - "ghcr.io/{{ .Env.DOCKER_USERNAME }}/{{ .Env.DOCKER_REPOSITORY }}:{{ .Env.TAG_VERSION }}-arm64" 46 | - "ghcr.io/{{ .Env.DOCKER_USERNAME }}/{{ .Env.DOCKER_REPOSITORY }}:latest" 47 | use: buildx 48 | dockerfile: ./deployments/Dockerfile 49 | goarch: arm64 50 | build_flag_templates: 51 | - --platform=linux/arm64/v8 52 | 53 | docker_manifests: 54 | - name_template: "ghcr.io/{{ .Env.DOCKER_USERNAME }}/{{ .Env.DOCKER_REPOSITORY }}:{{ .Env.TAG_VERSION }}" 55 | image_templates: 56 | - "ghcr.io/{{ .Env.DOCKER_USERNAME }}/{{ .Env.DOCKER_REPOSITORY }}:{{ .Env.TAG_VERSION }}-amd64" 57 | - "ghcr.io/{{ .Env.DOCKER_USERNAME }}/{{ .Env.DOCKER_REPOSITORY }}:{{ .Env.TAG_VERSION }}-arm64" 58 | - name_template: "ghcr.io/{{ .Env.DOCKER_USERNAME }}/{{ .Env.DOCKER_REPOSITORY }}:latest" 59 | image_templates: 60 | - "ghcr.io/{{ .Env.DOCKER_USERNAME }}/{{ .Env.DOCKER_REPOSITORY }}:{{ .Env.TAG_VERSION }}-amd64" 61 | - "ghcr.io/{{ .Env.DOCKER_USERNAME }}/{{ .Env.DOCKER_REPOSITORY }}:{{ .Env.TAG_VERSION }}-arm64" 62 | 63 | changelog: 64 | sort: asc 65 | filters: 66 | exclude: 67 | - "^docs" 68 | - "^test" 69 | - "^ci" 70 | - "^README" 71 | - "^Update" 72 | - Merge pull request 73 | - Merge branch 74 | -------------------------------------------------------------------------------- /internal/infrastructure/repository/source_repository_test.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | import ( 4 | "errors" 5 | "reflect" 6 | "testing" 7 | 8 | "github.com/fyvri/fresh-proxy-list/internal/entity" 9 | ) 10 | 11 | func TestNewSourceRepository(t *testing.T) { 12 | type args struct { 13 | proxy_resources string 14 | } 15 | 16 | tests := []struct { 17 | name string 18 | args args 19 | want SourceRepositoryInterface 20 | }{ 21 | { 22 | name: "Success", 23 | args: args{ 24 | proxy_resources: "", 25 | }, 26 | want: &SourceRepository{}, 27 | }, 28 | } 29 | 30 | for _, tt := range tests { 31 | t.Run(tt.name, func(t *testing.T) { 32 | sourceRepository := NewSourceRepository(tt.args.proxy_resources) 33 | 34 | if sourceRepository == nil { 35 | t.Errorf(expectedReturnNonNil, "NewSourceRepository", "SourceRepositoryInterface") 36 | } 37 | 38 | got, ok := sourceRepository.(*SourceRepository) 39 | if !ok { 40 | t.Errorf(expectedTypeAssertionErrorMessage, "*SourceRepository") 41 | } 42 | 43 | if !reflect.DeepEqual(tt.want, got) { 44 | t.Errorf(expectedButGotMessage, "*SourceRepository", tt.want, got) 45 | } 46 | }) 47 | } 48 | } 49 | 50 | func TestLoadSources(t *testing.T) { 51 | type args struct { 52 | proxy_resources string 53 | } 54 | 55 | tests := []struct { 56 | name string 57 | args args 58 | want []entity.Source 59 | wantErr error 60 | }{ 61 | { 62 | name: "EmptyResources", 63 | args: args{ 64 | proxy_resources: "", 65 | }, 66 | want: nil, 67 | wantErr: errors.New("PROXY_RESOURCES not found on environment"), 68 | }, 69 | { 70 | name: "InvalidJSON", 71 | args: args{ 72 | proxy_resources: `{"invalid": "json"`, 73 | }, 74 | want: nil, 75 | wantErr: errors.New("error parsing JSON: unexpected end of JSON input"), 76 | }, 77 | { 78 | name: "ValidJSON", 79 | args: args{ 80 | proxy_resources: `[{"method": "GET", "category": "general", "url": "http://example.com", "is_checked": true}]`, 81 | }, 82 | want: []entity.Source{ 83 | { 84 | Method: "GET", 85 | Category: "general", 86 | URL: "http://example.com", 87 | IsChecked: true, 88 | }, 89 | }, 90 | wantErr: nil, 91 | }, 92 | } 93 | 94 | for _, tt := range tests { 95 | t.Run(tt.name, func(t *testing.T) { 96 | r := &SourceRepository{ 97 | ProxyResources: tt.args.proxy_resources, 98 | } 99 | got, err := r.LoadSources() 100 | 101 | if !reflect.DeepEqual(got, tt.want) { 102 | t.Errorf(expectedButGotMessage, "LoadSources()", tt.want, got) 103 | } 104 | 105 | if (err != nil && tt.wantErr != nil && err.Error() != tt.wantErr.Error()) || 106 | (err != nil && tt.wantErr == nil) || 107 | (err == nil && tt.wantErr != nil) { 108 | t.Errorf(expectedErrorButGotMessage, "LoadSources()", tt.wantErr, err) 109 | } 110 | }) 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /internal/usecase/file_usecase_test.go: -------------------------------------------------------------------------------- 1 | package usecase 2 | 3 | import ( 4 | "path/filepath" 5 | "strings" 6 | "sync" 7 | "testing" 8 | 9 | "github.com/fyvri/fresh-proxy-list/internal/entity" 10 | ) 11 | 12 | var ( 13 | mutex sync.Mutex 14 | ) 15 | 16 | func TestSaveFiles(t *testing.T) { 17 | mockFileRepository := &mockFileRepository{} 18 | mockProxyRepository := &mockProxyRepository{} 19 | 20 | mockProxyRepository.GetAllClassicViewFunc = func() []string { 21 | return []string{ 22 | testProxy1, 23 | testProxy2, 24 | testProxy3, 25 | testProxy4, 26 | } 27 | } 28 | mockProxyRepository.GetAllAdvancedViewFunc = func() []entity.AdvancedProxy { 29 | return []entity.AdvancedProxy{ 30 | testAdvancedProxyEntity1, 31 | testAdvancedProxyEntity2, 32 | testAdvancedProxyEntity3, 33 | testAdvancedProxyEntity4, 34 | } 35 | } 36 | 37 | mockProxyRepository.GetHTTPClassicViewFunc = func() []string { 38 | return []string{testProxy1} 39 | } 40 | mockProxyRepository.GetHTTPAdvancedViewFunc = func() []entity.Proxy { 41 | return []entity.Proxy{ 42 | testProxyEntity1, 43 | } 44 | } 45 | 46 | mockProxyRepository.GetHTTPSClassicViewFunc = func() []string { 47 | return []string{ 48 | testProxy2, 49 | } 50 | } 51 | mockProxyRepository.GetHTTPSAdvancedViewFunc = func() []entity.Proxy { 52 | return []entity.Proxy{ 53 | testProxyEntity2, 54 | } 55 | } 56 | 57 | mockProxyRepository.GetSOCKS4ClassicViewFunc = func() []string { 58 | return []string{ 59 | testProxy3, 60 | } 61 | } 62 | mockProxyRepository.GetSOCKS4AdvancedViewFunc = func() []entity.Proxy { 63 | return []entity.Proxy{ 64 | testProxyEntity3, 65 | } 66 | } 67 | 68 | mockProxyRepository.GetSOCKS5ClassicViewFunc = func() []string { 69 | return []string{ 70 | testProxy4, 71 | } 72 | } 73 | mockProxyRepository.GetSOCKS5AdvancedViewFunc = func() []entity.Proxy { 74 | return []entity.Proxy{ 75 | testProxyEntity4, 76 | } 77 | } 78 | 79 | got := 0 80 | mockFileRepository.SaveFileFunc = func(filename string, data interface{}, extension string) error { 81 | mutex.Lock() 82 | defer mutex.Unlock() 83 | 84 | got++ 85 | t.Logf("SaveFile called with filename: %s, extension: %s", filename, extension) 86 | if !strings.HasPrefix(filename, filepath.Join(testStorageDir, testClassicDir)) && 87 | !strings.HasPrefix(filename, filepath.Join(testStorageDir, testAdvancedDir)) { 88 | t.Errorf(unexpectedMessage, "filename", filename) 89 | } 90 | if extension != testCSVExtension && extension != testJSONExtension && extension != testXMLExtension && extension != testYAMLExtension && extension != testTXTExtension { 91 | t.Errorf(unexpectedMessage, "extension", extension) 92 | } 93 | return nil 94 | } 95 | uc := NewFileUsecase(mockFileRepository, mockProxyRepository, testFileOutputExtensions) 96 | uc.SaveFiles() 97 | 98 | // (5 categories * number of extensions * 2 file types (classic, advanced)) + (5 all * 1 extension txt * 1 file type classic) 99 | want := (5 * len(testFileOutputExtensions) * 2) + (5 * 1 * 1) 100 | if got != want { 101 | t.Errorf(expectedButGotMessage, "calls", want, got) 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /internal/entity/source_test.go: -------------------------------------------------------------------------------- 1 | package entity 2 | 3 | import ( 4 | "encoding/json" 5 | "strconv" 6 | "testing" 7 | ) 8 | 9 | var ( 10 | expectedButGotMessage = "Expected %v = %v, but got = %v" 11 | expectedErrorButGotMessage = "Expected %v error = %v, but got = %v" 12 | testMethod = "LIST" 13 | testCategory = "HTTP" 14 | testURL = "http://example.com" 15 | testIsChecked = false 16 | ) 17 | 18 | func TestUnmarshalJSONWithIsChecked(t *testing.T) { 19 | var ( 20 | source = Source{} 21 | data = []byte(`{ 22 | "method": "` + testMethod + `", 23 | "category": "` + testCategory + `", 24 | "url": "` + testURL + `", 25 | "is_checked": ` + strconv.FormatBool(testIsChecked) + ` 26 | }`) 27 | ) 28 | err := json.Unmarshal(data, &source) 29 | 30 | if err != nil { 31 | t.Errorf(expectedErrorButGotMessage, "unmarshal", nil, err) 32 | } 33 | 34 | if source.Method != testMethod { 35 | t.Errorf(expectedButGotMessage, "method", testMethod, source.Method) 36 | } 37 | 38 | if source.Category != testCategory { 39 | t.Errorf(expectedButGotMessage, "category", testCategory, source.Category) 40 | } 41 | 42 | if source.URL != testURL { 43 | t.Errorf(expectedButGotMessage, "url", testURL, source.Category) 44 | } 45 | 46 | if source.IsChecked != testIsChecked { 47 | t.Errorf(expectedButGotMessage, "is_checked", testIsChecked, source.IsChecked) 48 | } 49 | } 50 | 51 | func TestUnmarshalJSONWithoutIsChecked(t *testing.T) { 52 | var ( 53 | source = Source{} 54 | data = []byte(`{ 55 | "method": "` + testMethod + `", 56 | "category": "` + testCategory + `", 57 | "url": "` + testURL + `" 58 | }`) 59 | ) 60 | err := json.Unmarshal(data, &source) 61 | 62 | if err != nil { 63 | t.Errorf(expectedErrorButGotMessage, "unmarshal", nil, err) 64 | } 65 | 66 | if source.Method != testMethod { 67 | t.Errorf(expectedButGotMessage, "method", testMethod, source.Method) 68 | } 69 | 70 | if source.Category != testCategory { 71 | t.Errorf(expectedButGotMessage, "category", testCategory, source.Category) 72 | } 73 | 74 | if source.URL != testURL { 75 | t.Errorf(expectedButGotMessage, "url", testURL, source.Category) 76 | } 77 | 78 | if source.IsChecked != true { 79 | t.Errorf(expectedButGotMessage, "is_checked", true, source.IsChecked) 80 | } 81 | } 82 | 83 | func TestUnmarshalJSONWithInvalidData(t *testing.T) { 84 | var ( 85 | source = Source{} 86 | data = []byte(`{ 87 | "method": "` + testMethod + `", 88 | "category": "` + testCategory + `", 89 | "url": "` + testURL + `", 90 | "is_checked": "string_instead_of_bool" 91 | }`) 92 | ) 93 | err := json.Unmarshal(data, &source) 94 | if err == nil { 95 | t.Errorf(expectedButGotMessage, "unmarshal", "any error", err) 96 | } 97 | } 98 | 99 | func TestUnmarshalJSONWithEmptyData(t *testing.T) { 100 | var ( 101 | source = Source{} 102 | data = []byte(`{}`) 103 | ) 104 | err := json.Unmarshal(data, &source) 105 | 106 | if err != nil { 107 | t.Errorf(expectedErrorButGotMessage, "unmarshal", nil, err) 108 | } 109 | 110 | if source.Method != "" { 111 | t.Errorf(expectedButGotMessage, "method", "empty", source.Method) 112 | } 113 | 114 | if source.Category != "" { 115 | t.Errorf(expectedButGotMessage, "category", "empty", source.Category) 116 | } 117 | 118 | if source.URL != "" { 119 | t.Errorf(expectedButGotMessage, "url", "empty", source.Category) 120 | } 121 | 122 | if source.IsChecked != true { 123 | t.Errorf(expectedButGotMessage, "is_checked", true, source.IsChecked) 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /internal/usecase/proxy_usecase.go: -------------------------------------------------------------------------------- 1 | package usecase 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | "regexp" 7 | "slices" 8 | "strconv" 9 | "strings" 10 | "sync" 11 | 12 | "github.com/fyvri/fresh-proxy-list/internal/entity" 13 | "github.com/fyvri/fresh-proxy-list/internal/infrastructure/repository" 14 | "github.com/fyvri/fresh-proxy-list/internal/service" 15 | ) 16 | 17 | type ProxyUsecase struct { 18 | ProxyRepository repository.ProxyRepositoryInterface 19 | ProxyService service.ProxyServiceInterface 20 | ProxyMap sync.Map 21 | SpecialIPs []string 22 | PrivateIPs []net.IPNet 23 | } 24 | 25 | type ProxyUsecaseInterface interface { 26 | ProcessProxy(category string, proxy string, isChecked bool) (*entity.Proxy, error) 27 | IsSpecialIP(ip string) bool 28 | GetAllAdvancedView() []entity.AdvancedProxy 29 | } 30 | 31 | func NewProxyUsecase( 32 | proxyRepository repository.ProxyRepositoryInterface, 33 | proxyService service.ProxyServiceInterface, 34 | specialIPs []string, 35 | privateIPs []net.IPNet, 36 | ) ProxyUsecaseInterface { 37 | return &ProxyUsecase{ 38 | ProxyRepository: proxyRepository, 39 | ProxyService: proxyService, 40 | SpecialIPs: specialIPs, 41 | PrivateIPs: privateIPs, 42 | ProxyMap: sync.Map{}, 43 | } 44 | } 45 | 46 | func (uc *ProxyUsecase) ProcessProxy(category string, proxy string, isChecked bool) (*entity.Proxy, error) { 47 | proxy = strings.TrimSpace(strings.ReplaceAll(strings.ReplaceAll(proxy, "\r", ""), "\n", "")) 48 | if proxy == "" { 49 | return nil, fmt.Errorf("proxy not found") 50 | } 51 | 52 | proxyParts := strings.Split(proxy, ":") 53 | if len(proxyParts) != 2 { 54 | return nil, fmt.Errorf("proxy format incorrect") 55 | } 56 | 57 | pattern := `^((25[0-5]|2[0-4][0-9]|[0-1]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[0-1]?[0-9][0-9]?)\:(0|[1-9][0-9]{0,4})$` 58 | re := regexp.MustCompile(pattern) 59 | if !re.MatchString(proxy) { 60 | return nil, fmt.Errorf("proxy format not match") 61 | } 62 | 63 | if uc.IsSpecialIP(proxyParts[0]) { 64 | return nil, fmt.Errorf("proxy belongs to special ip") 65 | } 66 | 67 | port, err := strconv.Atoi(proxyParts[1]) 68 | if err != nil || port < 0 || port > 65535 { 69 | return nil, fmt.Errorf("proxy port format incorrect") 70 | } 71 | 72 | _, loaded := uc.ProxyMap.LoadOrStore(category+"_"+proxy, true) 73 | if loaded { 74 | return nil, fmt.Errorf("proxy has been processed") 75 | } 76 | 77 | var ( 78 | data *entity.Proxy 79 | proxyIP, proxyPort = proxyParts[0], proxyParts[1] 80 | ) 81 | if isChecked { 82 | data, err = uc.ProxyService.Check(category, proxyIP, proxyPort) 83 | if err != nil { 84 | return nil, err 85 | } 86 | } else { 87 | data = &entity.Proxy{ 88 | Proxy: proxy, 89 | IP: proxyIP, 90 | Port: proxyPort, 91 | Category: category, 92 | TimeTaken: 0, 93 | CheckedAt: "", 94 | } 95 | } 96 | uc.ProxyRepository.Store(data) 97 | 98 | return data, nil 99 | } 100 | 101 | func (uc *ProxyUsecase) IsSpecialIP(ip string) bool { 102 | if _, found := slices.BinarySearch(uc.SpecialIPs, ip); found { 103 | return true 104 | } 105 | 106 | ipAddress := net.ParseIP(ip) 107 | if ipAddress == nil { 108 | return true 109 | } 110 | 111 | if ipAddress.IsLoopback() || ipAddress.IsMulticast() || ipAddress.IsUnspecified() { 112 | return true 113 | } 114 | 115 | for _, r := range uc.PrivateIPs { 116 | if r.Contains(ipAddress) { 117 | return true 118 | } 119 | } 120 | 121 | return false 122 | } 123 | 124 | func (uc *ProxyUsecase) GetAllAdvancedView() []entity.AdvancedProxy { 125 | return uc.ProxyRepository.GetAllAdvancedView() 126 | } 127 | -------------------------------------------------------------------------------- /pkg/utils/csv_writer_util_test.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "bytes" 5 | "encoding/csv" 6 | "io" 7 | "reflect" 8 | "testing" 9 | ) 10 | 11 | var ( 12 | writerBufferSize = 1024 13 | ) 14 | 15 | func TestNewCSVWriter(t *testing.T) { 16 | tests := []struct { 17 | name string 18 | want CSVWriterUtilInterface 19 | }{ 20 | { 21 | name: "Success", 22 | want: &CSVWriterUtil{}, 23 | }, 24 | } 25 | 26 | for _, tt := range tests { 27 | t.Run(tt.name, func(t *testing.T) { 28 | csvWriterUtil := NewCSVWriter() 29 | if csvWriterUtil == nil { 30 | t.Errorf(expectedReturnNonNil, "NewCSVWriter", "CSVWriterUtilInterface") 31 | } 32 | 33 | got, ok := csvWriterUtil.(*CSVWriterUtil) 34 | if !ok { 35 | t.Errorf(expectedTypeAssertionErrorMessage, "*CSVWriterUtil") 36 | } 37 | 38 | if !reflect.DeepEqual(tt.want, got) { 39 | t.Errorf(expectedButGotMessage, "*CSVWriterUtil", tt.want, got) 40 | } 41 | }) 42 | } 43 | } 44 | 45 | func TestInit(t *testing.T) { 46 | type args struct { 47 | writer io.Writer 48 | } 49 | 50 | tests := []struct { 51 | name string 52 | args args 53 | want *csv.Writer 54 | }{ 55 | { 56 | name: "Success", 57 | args: args{ 58 | writer: bytes.NewBuffer(make([]byte, writerBufferSize)), 59 | }, 60 | want: csv.NewWriter(bytes.NewBuffer(make([]byte, writerBufferSize))), 61 | }, 62 | } 63 | 64 | for _, tt := range tests { 65 | t.Run(tt.name, func(t *testing.T) { 66 | u := NewCSVWriter() 67 | got := u.Init(tt.args.writer) 68 | if reflect.TypeOf(got) != reflect.TypeOf(tt.want) { 69 | t.Errorf(expectedButGotMessage, "Init()", reflect.TypeOf(tt.want), reflect.TypeOf(got)) 70 | } 71 | }) 72 | } 73 | } 74 | 75 | func TestFlush(t *testing.T) { 76 | type setup struct { 77 | newCSVWriter func() *CSVWriterUtil 78 | } 79 | 80 | type args struct { 81 | csvWriter *csv.Writer 82 | } 83 | 84 | tests := []struct { 85 | name string 86 | setup setup 87 | args args 88 | }{ 89 | { 90 | name: "Success", 91 | setup: setup{ 92 | newCSVWriter: func() *CSVWriterUtil { 93 | return NewCSVWriter().(*CSVWriterUtil) 94 | }, 95 | }, 96 | args: args{ 97 | csvWriter: csv.NewWriter(bytes.NewBuffer(make([]byte, writerBufferSize))), 98 | }, 99 | }, 100 | } 101 | 102 | for _, tt := range tests { 103 | t.Run(tt.name, func(t *testing.T) { 104 | u := tt.setup.newCSVWriter() 105 | u.Flush(tt.args.csvWriter) 106 | }) 107 | } 108 | } 109 | 110 | func TestWrite(t *testing.T) { 111 | type setup struct { 112 | newCSVWriter func() *CSVWriterUtil 113 | } 114 | 115 | type args struct { 116 | writer io.Writer 117 | record []string 118 | } 119 | 120 | tests := []struct { 121 | name string 122 | setup setup 123 | args args 124 | wantError error 125 | }{ 126 | { 127 | name: "Success", 128 | setup: setup{ 129 | newCSVWriter: func() *CSVWriterUtil { 130 | return NewCSVWriter().(*CSVWriterUtil) 131 | }, 132 | }, 133 | args: args{ 134 | writer: &bytes.Buffer{}, 135 | record: []string{"a", "b", "c"}, 136 | }, 137 | wantError: nil, 138 | }, 139 | } 140 | 141 | for _, tt := range tests { 142 | t.Run(tt.name, func(t *testing.T) { 143 | u := tt.setup.newCSVWriter() 144 | csvWriter := u.Init(tt.args.writer) 145 | err := u.Write(csvWriter, tt.args.record) 146 | if (err != nil && tt.wantError != nil && err.Error() != tt.wantError.Error()) || 147 | (err == nil && tt.wantError != nil) || 148 | (err != nil && tt.wantError == nil) { 149 | t.Errorf(expectedErrorButGotMessage, "Write()", tt.wantError, err) 150 | } 151 | }) 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /cmd/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "io" 5 | "log" 6 | "net/http" 7 | "os" 8 | "slices" 9 | "sync" 10 | "time" 11 | 12 | "github.com/fyvri/fresh-proxy-list/internal/entity" 13 | "github.com/fyvri/fresh-proxy-list/internal/infrastructure/config" 14 | "github.com/fyvri/fresh-proxy-list/internal/infrastructure/repository" 15 | "github.com/fyvri/fresh-proxy-list/internal/service" 16 | "github.com/fyvri/fresh-proxy-list/internal/usecase" 17 | "github.com/fyvri/fresh-proxy-list/pkg/utils" 18 | 19 | "github.com/joho/godotenv" 20 | ) 21 | 22 | type Runners struct { 23 | fetcherUtil utils.FetcherUtilInterface 24 | urlParserUtil utils.URLParserUtilInterface 25 | proxyService service.ProxyServiceInterface 26 | sourceRepository repository.SourceRepositoryInterface 27 | proxyRepository repository.ProxyRepositoryInterface 28 | fileRepository repository.FileRepositoryInterface 29 | } 30 | 31 | func main() { 32 | if err := runApplication(); err != nil { 33 | log.Fatalf("Application error: %v", err) 34 | } 35 | } 36 | 37 | func runApplication() error { 38 | loadEnv() 39 | 40 | httpTestingSites := config.HTTPTestingSites 41 | httpsTestingSites := config.HTTPSTestingSites 42 | userAgents := config.UserAgents 43 | 44 | mkdirAll := func(path string, perm os.FileMode) error { 45 | return os.MkdirAll(path, perm) 46 | } 47 | create := func(name string) (io.Writer, error) { 48 | file, err := os.Create(name) 49 | if err != nil { 50 | return nil, err 51 | } 52 | return file, nil 53 | } 54 | 55 | fetcherUtil := utils.NewFetcher(http.DefaultClient, http.NewRequest) 56 | urlParserUtil := utils.NewURLParser() 57 | csvWriterUtil := utils.NewCSVWriter() 58 | proxyService := service.NewProxyService(fetcherUtil, urlParserUtil, httpTestingSites, httpsTestingSites, userAgents) 59 | sourceRepository := repository.NewSourceRepository(os.Getenv("PROXY_RESOURCES")) 60 | proxyRepository := repository.NewProxyRepository() 61 | fileRepository := repository.NewFileRepository(mkdirAll, create, csvWriterUtil) 62 | 63 | runners := Runners{ 64 | fetcherUtil: fetcherUtil, 65 | urlParserUtil: urlParserUtil, 66 | proxyService: proxyService, 67 | sourceRepository: sourceRepository, 68 | proxyRepository: proxyRepository, 69 | fileRepository: fileRepository, 70 | } 71 | 72 | return run(runners) 73 | } 74 | 75 | func loadEnv() error { 76 | return godotenv.Load() 77 | } 78 | 79 | func run(runners Runners) error { 80 | startTime := time.Now() 81 | 82 | sourceUsecase := usecase.NewSourceUsecase(runners.sourceRepository, runners.fetcherUtil) 83 | sources, err := sourceUsecase.LoadSources() 84 | if err != nil { 85 | return err 86 | } 87 | 88 | wg := sync.WaitGroup{} 89 | proxyCategories := config.ProxyCategories 90 | specialIPs := config.SpecialIPs 91 | privateIPs := config.PrivateIPs 92 | proxyUsecase := usecase.NewProxyUsecase(runners.proxyRepository, runners.proxyService, specialIPs, privateIPs) 93 | for i, source := range sources { 94 | if _, found := slices.BinarySearch(proxyCategories, source.Category); found { 95 | wg.Add(1) 96 | go func(source entity.Source) { 97 | defer wg.Done() 98 | 99 | proxies, err := sourceUsecase.ProcessSource(&source) 100 | if err != nil { 101 | return 102 | } 103 | 104 | innerWG := sync.WaitGroup{} 105 | for _, proxy := range proxies { 106 | innerWG.Add(1) 107 | go func(source entity.Source, proxy string) { 108 | defer innerWG.Done() 109 | proxyUsecase.ProcessProxy(source.Category, proxy, source.IsChecked) 110 | }(source, proxy) 111 | } 112 | innerWG.Wait() 113 | }(source) 114 | } else { 115 | log.Printf("Index %v: proxy category not found", i) 116 | } 117 | } 118 | wg.Wait() 119 | 120 | fileOutputExtensions := config.FileOutputExtensions 121 | fileUsecase := usecase.NewFileUsecase(runners.fileRepository, runners.proxyRepository, fileOutputExtensions) 122 | fileUsecase.SaveFiles() 123 | 124 | log.Printf("Number of proxies : %v", len(proxyUsecase.GetAllAdvancedView())) 125 | log.Printf("Time-consuming process: %v", time.Since(startTime)) 126 | return nil 127 | } 128 | -------------------------------------------------------------------------------- /internal/infrastructure/repository/proxy_repository.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | import ( 4 | "cmp" 5 | "slices" 6 | "sync" 7 | 8 | "github.com/fyvri/fresh-proxy-list/internal/entity" 9 | ) 10 | 11 | type ProxyRepository struct { 12 | Mutex sync.RWMutex 13 | AllClassicView []string 14 | HTTPClassicView []string 15 | HTTPSClassicView []string 16 | SOCKS4ClassicView []string 17 | SOCKS5ClassicView []string 18 | AllAdvancedView []entity.AdvancedProxy 19 | HTTPAdvancedView []entity.Proxy 20 | HTTPSAdvancedView []entity.Proxy 21 | SOCKS4AdvancedView []entity.Proxy 22 | SOCKS5AdvancedView []entity.Proxy 23 | } 24 | 25 | type ProxyRepositoryInterface interface { 26 | Store(proxy *entity.Proxy) 27 | GetAllClassicView() []string 28 | GetHTTPClassicView() []string 29 | GetHTTPSClassicView() []string 30 | GetSOCKS4ClassicView() []string 31 | GetSOCKS5ClassicView() []string 32 | GetAllAdvancedView() []entity.AdvancedProxy 33 | GetHTTPAdvancedView() []entity.Proxy 34 | GetHTTPSAdvancedView() []entity.Proxy 35 | GetSOCKS4AdvancedView() []entity.Proxy 36 | GetSOCKS5AdvancedView() []entity.Proxy 37 | } 38 | 39 | func NewProxyRepository() ProxyRepositoryInterface { 40 | return &ProxyRepository{ 41 | Mutex: sync.RWMutex{}, 42 | } 43 | } 44 | 45 | func (r *ProxyRepository) Store(proxy *entity.Proxy) { 46 | r.Mutex.Lock() 47 | defer r.Mutex.Unlock() 48 | 49 | updateProxyAll := func(proxy *entity.Proxy, classicList *[]string, advancedList *[]entity.AdvancedProxy) { 50 | n, found := slices.BinarySearchFunc(*advancedList, entity.AdvancedProxy{Proxy: proxy.Proxy}, func(a, b entity.AdvancedProxy) int { 51 | return cmp.Compare(a.Proxy, b.Proxy) 52 | }) 53 | if found { 54 | if proxy.Category == "HTTP" && proxy.TimeTaken > 0 { 55 | (*advancedList)[n].TimeTaken = proxy.TimeTaken 56 | } 57 | 58 | if m, found := slices.BinarySearch((*advancedList)[n].Categories, proxy.Category); !found { 59 | (*advancedList)[n].Categories = slices.Insert((*advancedList)[n].Categories, m, proxy.Category) 60 | } 61 | } else { 62 | *classicList = append(*classicList, proxy.Proxy) 63 | *advancedList = slices.Insert(*advancedList, n, entity.AdvancedProxy{ 64 | Proxy: proxy.Proxy, 65 | IP: proxy.IP, 66 | Port: proxy.Port, 67 | TimeTaken: proxy.TimeTaken, 68 | CheckedAt: proxy.CheckedAt, 69 | Categories: []string{ 70 | proxy.Category, 71 | }, 72 | }) 73 | } 74 | } 75 | 76 | switch proxy.Category { 77 | case "HTTP": 78 | r.HTTPClassicView = append(r.HTTPClassicView, proxy.Proxy) 79 | r.HTTPAdvancedView = append(r.HTTPAdvancedView, *proxy) 80 | case "HTTPS": 81 | r.HTTPSClassicView = append(r.HTTPSClassicView, proxy.Proxy) 82 | r.HTTPSAdvancedView = append(r.HTTPSAdvancedView, *proxy) 83 | case "SOCKS4": 84 | r.SOCKS4ClassicView = append(r.SOCKS4ClassicView, proxy.Proxy) 85 | r.SOCKS4AdvancedView = append(r.SOCKS4AdvancedView, *proxy) 86 | case "SOCKS5": 87 | r.SOCKS5ClassicView = append(r.SOCKS5ClassicView, proxy.Proxy) 88 | r.SOCKS5AdvancedView = append(r.SOCKS5AdvancedView, *proxy) 89 | } 90 | 91 | updateProxyAll(proxy, &r.AllClassicView, &r.AllAdvancedView) 92 | } 93 | 94 | func (r *ProxyRepository) GetAllClassicView() []string { 95 | return r.AllClassicView 96 | } 97 | 98 | func (r *ProxyRepository) GetHTTPClassicView() []string { 99 | return r.HTTPClassicView 100 | } 101 | 102 | func (r *ProxyRepository) GetHTTPSClassicView() []string { 103 | return r.HTTPSClassicView 104 | } 105 | 106 | func (r *ProxyRepository) GetSOCKS4ClassicView() []string { 107 | return r.SOCKS4ClassicView 108 | } 109 | 110 | func (r *ProxyRepository) GetSOCKS5ClassicView() []string { 111 | return r.SOCKS5ClassicView 112 | } 113 | 114 | func (r *ProxyRepository) GetAllAdvancedView() []entity.AdvancedProxy { 115 | return r.AllAdvancedView 116 | } 117 | 118 | func (r *ProxyRepository) GetHTTPAdvancedView() []entity.Proxy { 119 | return r.HTTPAdvancedView 120 | } 121 | 122 | func (r *ProxyRepository) GetHTTPSAdvancedView() []entity.Proxy { 123 | return r.HTTPSAdvancedView 124 | } 125 | 126 | func (r *ProxyRepository) GetSOCKS4AdvancedView() []entity.Proxy { 127 | return r.SOCKS4AdvancedView 128 | } 129 | 130 | func (r *ProxyRepository) GetSOCKS5AdvancedView() []entity.Proxy { 131 | return r.SOCKS5AdvancedView 132 | } 133 | -------------------------------------------------------------------------------- /internal/service/proxy_service.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "crypto/tls" 5 | "fmt" 6 | "math/rand" 7 | "net" 8 | "net/http" 9 | "strings" 10 | "time" 11 | 12 | "github.com/fyvri/fresh-proxy-list/internal/entity" 13 | "github.com/fyvri/fresh-proxy-list/pkg/utils" 14 | 15 | "h12.io/socks" 16 | ) 17 | 18 | type ProxyService struct { 19 | FetcherUtil utils.FetcherUtilInterface 20 | URLParserUtil utils.URLParserUtilInterface 21 | HTTPTestingSites []string 22 | HTTPSTestingSites []string 23 | UserAgents []string 24 | Semaphore chan struct{} 25 | } 26 | 27 | type ProxyServiceInterface interface { 28 | Check(category string, ip string, port string) (*entity.Proxy, error) 29 | GetTestingSite(category string) string 30 | GetRandomUserAgent() string 31 | } 32 | 33 | func NewProxyService( 34 | fetcherUtil utils.FetcherUtilInterface, 35 | urlParserUtil utils.URLParserUtilInterface, 36 | httpTestingSites []string, 37 | httpsTestingSites []string, 38 | userAgents []string, 39 | ) ProxyServiceInterface { 40 | return &ProxyService{ 41 | FetcherUtil: fetcherUtil, 42 | URLParserUtil: urlParserUtil, 43 | HTTPTestingSites: httpTestingSites, 44 | HTTPSTestingSites: httpsTestingSites, 45 | UserAgents: userAgents, 46 | Semaphore: make(chan struct{}, 500), 47 | } 48 | } 49 | 50 | func (s *ProxyService) Check(category string, ip string, port string) (*entity.Proxy, error) { 51 | s.Semaphore <- struct{}{} 52 | defer func() { <-s.Semaphore }() 53 | 54 | var ( 55 | transport *http.Transport 56 | proxy = ip + ":" + port 57 | proxyURI = strings.ToLower(category + "://" + proxy) 58 | testingSite = s.GetTestingSite(category) 59 | timeout = 60 * time.Second 60 | ) 61 | 62 | if category == "HTTP" || category == "HTTPS" { 63 | proxyURL, err := s.URLParserUtil.Parse(proxyURI) 64 | if err != nil { 65 | return nil, fmt.Errorf("error parsing proxy URL: %v", err) 66 | } 67 | 68 | transport = &http.Transport{ 69 | Proxy: http.ProxyURL(proxyURL), 70 | DisableKeepAlives: true, 71 | DialContext: (&net.Dialer{ 72 | Timeout: timeout, 73 | KeepAlive: timeout, 74 | }).DialContext, 75 | TLSHandshakeTimeout: timeout, 76 | TLSClientConfig: &tls.Config{ 77 | InsecureSkipVerify: category == "HTTPS", 78 | }, 79 | } 80 | } else if category == "SOCKS4" || category == "SOCKS5" { 81 | proxyURL := socks.Dial(proxyURI) 82 | transport = &http.Transport{ 83 | Dial: proxyURL, 84 | DisableKeepAlives: true, 85 | DialContext: (&net.Dialer{ 86 | Timeout: timeout, 87 | KeepAlive: timeout, 88 | }).DialContext, 89 | } 90 | } else { 91 | return nil, fmt.Errorf("proxy category %s not supported", category) 92 | } 93 | 94 | req, err := s.FetcherUtil.NewRequest("GET", testingSite, nil) 95 | if err != nil { 96 | return nil, fmt.Errorf("error creating request: %s", err) 97 | } 98 | req.Header.Set("User-Agent", s.GetRandomUserAgent()) 99 | 100 | startTime := time.Now() 101 | resp, err := s.FetcherUtil.Do(&http.Client{ 102 | Transport: transport, 103 | Timeout: timeout, 104 | }, req) 105 | 106 | // statusCode := "" 107 | // if err == nil { 108 | // statusCode = http.StatusText(resp.StatusCode) 109 | // } 110 | // log.Printf("Check %s: %s ~> %s ~> %v", fmt.Sprintf("%-25s", proxy), fmt.Sprintf("%-30s", statusCode), testingSite, err) 111 | 112 | if err != nil { 113 | return nil, fmt.Errorf("request error: %s", err) 114 | } 115 | defer resp.Body.Close() 116 | endTime := time.Now() 117 | timeTaken := endTime.Sub(startTime).Seconds() 118 | 119 | if resp.StatusCode != http.StatusOK { 120 | return nil, fmt.Errorf("unexpected status code %d: %s", resp.StatusCode, http.StatusText(resp.StatusCode)) 121 | } 122 | 123 | return &entity.Proxy{ 124 | Proxy: proxy, 125 | IP: ip, 126 | Port: port, 127 | Category: category, 128 | CheckedAt: endTime.Format(time.RFC3339), 129 | TimeTaken: timeTaken, 130 | }, nil 131 | } 132 | 133 | func (s *ProxyService) GetTestingSite(category string) string { 134 | if category == "HTTPS" { 135 | return s.HTTPSTestingSites[rand.Intn(len(s.HTTPSTestingSites))] 136 | } 137 | return s.HTTPTestingSites[rand.Intn(len(s.HTTPTestingSites))] 138 | } 139 | 140 | func (s *ProxyService) GetRandomUserAgent() string { 141 | return s.UserAgents[rand.Intn(len(s.UserAgents))] 142 | } 143 | -------------------------------------------------------------------------------- /internal/infrastructure/repository/repository_test.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | import ( 4 | "encoding/csv" 5 | "encoding/json" 6 | "io" 7 | "time" 8 | 9 | "github.com/fyvri/fresh-proxy-list/internal/entity" 10 | ) 11 | 12 | var ( 13 | expectedButGotMessage = "Expected %v = %v, but got = %v" 14 | expectedErrorButGotMessage = "Expected %v error = %v, but got = %v" 15 | expectedTypeAssertionErrorMessage = "Expected type assertion error, but got = %v" 16 | expectedReturnNonNil = "Expected %v to return a non-nil %v" 17 | testErrorWriting = "error writing" 18 | testErrorEncode = "error encoding %s: %s" 19 | testStorageDir = "/tmp" 20 | testClassicDir = "/classic" 21 | testAdvancedDir = "/advanced" 22 | testClassicFilePath = testStorageDir + testClassicDir + "/test_file" 23 | testAdvancedFilePath = testStorageDir + testAdvancedDir + "/test_file" 24 | testTXTExtension = "txt" 25 | testCSVExtension = "csv" 26 | testJSONExtension = "json" 27 | testXMLExtension = "xml" 28 | testYAMLExtension = "yaml" 29 | testHTTPCategory = "HTTP" 30 | testHTTPSCategory = "HTTPS" 31 | testSOCKS4Category = "SOCKS4" 32 | testSOCKS5Category = "SOCKS5" 33 | testTimeTaken = 1.2345 34 | testCheckedAt = "2024-07-27T00:00:00Z" 35 | 36 | testIP1 = "13.37.0.1" 37 | testPort1 = "1337" 38 | testProxy1 = testIP1 + ":" + testPort1 39 | testCategory1 = testHTTPCategory 40 | testProxyEntity1 = entity.Proxy{ 41 | Proxy: testProxy1, 42 | IP: testIP1, 43 | Port: testPort1, 44 | Category: testCategory1, 45 | TimeTaken: 0, 46 | CheckedAt: time.Now().Format(time.RFC3339), 47 | } 48 | testAdvancedProxyEntity1 = entity.AdvancedProxy{ 49 | Proxy: testProxyEntity1.Proxy, 50 | IP: testProxyEntity1.IP, 51 | Port: testProxyEntity1.Port, 52 | TimeTaken: testProxyEntity1.TimeTaken, 53 | CheckedAt: testProxyEntity1.CheckedAt, 54 | Categories: []string{ 55 | testCategory1, 56 | }, 57 | } 58 | 59 | testIP2 = "13.37.0.2" 60 | testPort2 = "1337" 61 | testProxy2 = testIP2 + ":" + testPort2 62 | testCategory2 = testHTTPSCategory 63 | testProxyEntity2 = entity.Proxy{ 64 | Proxy: testProxy2, 65 | IP: testIP2, 66 | Port: testPort2, 67 | Category: testCategory2, 68 | TimeTaken: 0, 69 | CheckedAt: time.Now().Format(time.RFC3339), 70 | } 71 | testAdvancedProxyEntity2 = entity.AdvancedProxy{ 72 | Proxy: testProxyEntity2.Proxy, 73 | IP: testProxyEntity2.IP, 74 | Port: testProxyEntity2.Port, 75 | TimeTaken: testProxyEntity2.TimeTaken, 76 | CheckedAt: testProxyEntity2.CheckedAt, 77 | Categories: []string{ 78 | testCategory2, 79 | }, 80 | } 81 | 82 | testIP3 = "13.37.0.3" 83 | testPort3 = "1337" 84 | testProxy3 = testIP3 + ":" + testPort3 85 | testCategory3 = testSOCKS4Category 86 | testProxyEntity3 = entity.Proxy{ 87 | Proxy: testProxy3, 88 | IP: testIP3, 89 | Port: testPort3, 90 | Category: testCategory3, 91 | TimeTaken: 0, 92 | CheckedAt: time.Now().Format(time.RFC3339), 93 | } 94 | testAdvancedProxyEntity3 = entity.AdvancedProxy{ 95 | Proxy: testProxyEntity3.Proxy, 96 | IP: testProxyEntity3.IP, 97 | Port: testProxyEntity3.Port, 98 | TimeTaken: testProxyEntity3.TimeTaken, 99 | CheckedAt: testProxyEntity3.CheckedAt, 100 | Categories: []string{ 101 | testCategory3, 102 | }, 103 | } 104 | 105 | testIP4 = "13.37.0.4" 106 | testPort4 = "1337" 107 | testProxy4 = testIP4 + ":" + testPort4 108 | testCategory4 = testSOCKS5Category 109 | testProxyEntity4 = entity.Proxy{ 110 | Proxy: testProxy4, 111 | IP: testIP4, 112 | Port: testPort4, 113 | Category: testCategory4, 114 | TimeTaken: 0, 115 | CheckedAt: time.Now().Format(time.RFC3339), 116 | } 117 | testAdvancedProxyEntity4 = entity.AdvancedProxy{ 118 | Proxy: testProxyEntity4.Proxy, 119 | IP: testProxyEntity4.IP, 120 | Port: testProxyEntity4.Port, 121 | TimeTaken: testProxyEntity4.TimeTaken, 122 | CheckedAt: testProxyEntity4.CheckedAt, 123 | Categories: []string{ 124 | testCategory4, 125 | }, 126 | } 127 | 128 | testIPs = []string{ 129 | testIP1, 130 | testIP2, 131 | testIP3, 132 | testIP4, 133 | } 134 | testProxies = []entity.Proxy{ 135 | testProxyEntity1, 136 | testProxyEntity2, 137 | testProxyEntity3, 138 | testProxyEntity4, 139 | } 140 | testAdvancedProxies = []entity.AdvancedProxy{ 141 | testAdvancedProxyEntity1, 142 | testAdvancedProxyEntity2, 143 | testAdvancedProxyEntity3, 144 | testAdvancedProxyEntity4, 145 | } 146 | testIPsToString, _ = json.Marshal(testIPs) 147 | testProxiesToString, _ = json.Marshal(testProxies) 148 | testAdvancedProxiesToString, _ = json.Marshal(testAdvancedProxies) 149 | ) 150 | 151 | type mockWriter struct { 152 | errWrite error 153 | errClose error 154 | } 155 | 156 | func (m *mockWriter) Write(p []byte) (int, error) { 157 | if m.errWrite != nil { 158 | return 0, m.errWrite 159 | } 160 | return len(p), nil 161 | } 162 | 163 | func (m *mockWriter) Close() error { 164 | if m.errClose != nil { 165 | return m.errClose 166 | } 167 | return nil 168 | } 169 | 170 | type mockCSVWriterUtil struct { 171 | errFlush error 172 | errWrite error 173 | } 174 | 175 | func (m *mockCSVWriterUtil) Init(w io.Writer) *csv.Writer { 176 | return csv.NewWriter(w) 177 | } 178 | 179 | func (m *mockCSVWriterUtil) Flush(csvWriter *csv.Writer) { 180 | if m.errFlush != nil { 181 | return 182 | } 183 | } 184 | 185 | func (m *mockCSVWriterUtil) Write(csvWriter *csv.Writer, record []string) error { 186 | if m.errWrite != nil { 187 | return m.errWrite 188 | } 189 | return nil 190 | } 191 | -------------------------------------------------------------------------------- /pkg/utils/fetcher_util_test.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "fmt" 7 | "io" 8 | "net/http" 9 | "reflect" 10 | "testing" 11 | ) 12 | 13 | var ( 14 | testGETMethod = "GET" 15 | testPOSTMethod = "POST" 16 | testClient = http.DefaultClient 17 | testNewRequest = http.NewRequest 18 | ) 19 | 20 | func TestNewFetcher(t *testing.T) { 21 | fetcherUtil := NewFetcher(testClient, testNewRequest) 22 | 23 | if fetcherUtil == nil { 24 | t.Errorf(expectedReturnNonNil, "NewFetcher", "FetcherInterface") 25 | } 26 | 27 | fetcherUtilInstance, ok := fetcherUtil.(*FetcherUtil) 28 | if !ok { 29 | t.Errorf(expectedTypeAssertionErrorMessage, "*FetcherUtil") 30 | } 31 | 32 | req, err := fetcherUtilInstance.NewRequestFunc(testGETMethod, testRawURL, nil) 33 | if err != nil { 34 | t.Errorf(expectedButGotMessage, "newRequest", "no error", err) 35 | } 36 | 37 | if req.Method != testGETMethod { 38 | t.Errorf(expectedButGotMessage, "method", testGETMethod, req.Method) 39 | } 40 | 41 | if req.URL.String() != testRawURL { 42 | t.Errorf(expectedButGotMessage, "URL", testRawURL, req.URL.String()) 43 | } 44 | 45 | if fetcherUtilInstance.Client != nil && fetcherUtilInstance.Client != http.DefaultClient { 46 | t.Errorf(expectedButGotMessage, "client", http.DefaultClient, fetcherUtilInstance.Client) 47 | } 48 | } 49 | 50 | func TestFetchData(t *testing.T) { 51 | type fields struct { 52 | transport *mockTransport 53 | newRequestFunc func(method string, url string, body io.Reader) (*http.Request, error) 54 | } 55 | 56 | type args struct { 57 | url string 58 | } 59 | 60 | tests := []struct { 61 | name string 62 | fields fields 63 | args args 64 | want []byte 65 | wantErr error 66 | }{ 67 | { 68 | name: "Success", 69 | fields: fields{ 70 | transport: &mockTransport{ 71 | response: &http.Response{ 72 | StatusCode: http.StatusOK, 73 | Body: io.NopCloser(&mockReadCloser{ 74 | data: []byte("response data"), 75 | }), 76 | }, 77 | err: nil, 78 | }, 79 | newRequestFunc: testNewRequest, 80 | }, 81 | args: args{ 82 | url: testRawURL, 83 | }, 84 | want: []byte("response data"), 85 | wantErr: nil, 86 | }, 87 | { 88 | name: "NewRequestError", 89 | fields: fields{ 90 | transport: &mockTransport{ 91 | response: nil, 92 | err: nil, 93 | }, 94 | newRequestFunc: func(method string, url string, body io.Reader) (*http.Request, error) { 95 | return nil, fmt.Errorf("new request error") 96 | }, 97 | }, 98 | args: args{ 99 | url: testRawURL, 100 | }, 101 | want: nil, 102 | wantErr: errors.New("new request error"), 103 | }, 104 | { 105 | name: "RequestError", 106 | fields: fields{ 107 | transport: &mockTransport{ 108 | response: nil, 109 | err: fmt.Errorf("request error"), 110 | }, 111 | newRequestFunc: testNewRequest, 112 | }, 113 | args: args{ 114 | url: testRawURL, 115 | }, 116 | want: nil, 117 | wantErr: fmt.Errorf("Get \"%s\": request error", testRawURL), 118 | }, 119 | { 120 | name: "ResponseError", 121 | fields: fields{ 122 | transport: &mockTransport{ 123 | response: &http.Response{ 124 | StatusCode: http.StatusInternalServerError, 125 | Body: io.NopCloser(&mockReadCloser{ 126 | data: []byte("error response"), 127 | }), 128 | }, 129 | err: nil, 130 | }, 131 | newRequestFunc: testNewRequest, 132 | }, 133 | args: args{ 134 | url: testRawURL, 135 | }, 136 | want: []byte("error response"), 137 | wantErr: errors.New("failed to fetch data: Internal Server Error"), 138 | }, 139 | { 140 | name: "ReadBodyError", 141 | fields: fields{ 142 | transport: &mockTransport{ 143 | response: &http.Response{ 144 | StatusCode: http.StatusOK, 145 | Body: &mockReadCloser{ 146 | errRead: fmt.Errorf("body read error"), 147 | }, 148 | }, 149 | err: nil, 150 | }, 151 | newRequestFunc: testNewRequest, 152 | }, 153 | args: args{ 154 | url: testRawURL, 155 | }, 156 | want: nil, 157 | wantErr: errors.New("body read error"), 158 | }, 159 | } 160 | 161 | for _, tt := range tests { 162 | t.Run(tt.name, func(t *testing.T) { 163 | fetcherUtil := &FetcherUtil{ 164 | Client: &http.Client{ 165 | Transport: tt.fields.transport, 166 | }, 167 | NewRequestFunc: tt.fields.newRequestFunc, 168 | } 169 | got, err := fetcherUtil.FetchData(tt.args.url) 170 | 171 | if (err != nil && tt.wantErr != nil && err.Error() != tt.wantErr.Error()) || 172 | (err == nil && tt.wantErr != nil) || 173 | (err != nil && tt.wantErr == nil) { 174 | t.Errorf(expectedErrorButGotMessage, "FetchData()", tt.wantErr, err) 175 | } 176 | 177 | if !reflect.DeepEqual(got, tt.want) { 178 | t.Errorf(expectedButGotMessage, "FetchData()", tt.want, got) 179 | } 180 | }) 181 | } 182 | } 183 | 184 | func TestNewRequest(t *testing.T) { 185 | type args struct { 186 | method string 187 | url string 188 | body io.Reader 189 | } 190 | 191 | type want struct { 192 | url string 193 | method string 194 | } 195 | 196 | tests := []struct { 197 | name string 198 | args args 199 | want want 200 | wantErr error 201 | }{ 202 | { 203 | name: testGETMethod, 204 | args: args{ 205 | method: http.MethodGet, 206 | url: testRawURL, 207 | body: nil, 208 | }, 209 | want: want{ 210 | url: testRawURL, 211 | method: http.MethodGet, 212 | }, 213 | wantErr: nil, 214 | }, 215 | { 216 | name: testPOSTMethod, 217 | args: args{ 218 | method: http.MethodPost, 219 | url: testRawURL, 220 | body: bytes.NewReader([]byte("body")), 221 | }, 222 | want: want{ 223 | url: testRawURL, 224 | method: http.MethodPost, 225 | }, 226 | }, 227 | } 228 | 229 | for _, tt := range tests { 230 | t.Run(tt.name, func(t *testing.T) { 231 | u := NewFetcher(&http.Client{ 232 | Transport: &mockTransport{}, 233 | }, testNewRequest) 234 | req, err := u.NewRequest(tt.args.method, tt.args.url, tt.args.body) 235 | 236 | if err != nil { 237 | t.Errorf(expectedErrorButGotMessage, "NewRequest()", nil, err) 238 | } 239 | 240 | if req.Method != tt.want.method { 241 | t.Errorf(expectedButGotMessage, "method", tt.want.method, req.Method) 242 | } 243 | 244 | if req.URL.String() != tt.want.url { 245 | t.Errorf(expectedButGotMessage, "URL", tt.want.url, req.URL.String()) 246 | } 247 | }) 248 | } 249 | } 250 | -------------------------------------------------------------------------------- /internal/usecase/source_usecase_test.go: -------------------------------------------------------------------------------- 1 | package usecase 2 | 3 | import ( 4 | "errors" 5 | "reflect" 6 | "testing" 7 | 8 | "github.com/fyvri/fresh-proxy-list/internal/entity" 9 | "github.com/fyvri/fresh-proxy-list/internal/infrastructure/repository" 10 | "github.com/fyvri/fresh-proxy-list/pkg/utils" 11 | ) 12 | 13 | var ( 14 | testListMethod = "LIST" 15 | testScrapMethod = "SCRAP" 16 | testCategory = "HTTP" 17 | testURL = "http://example.com" 18 | testIsChecked = true 19 | ) 20 | 21 | func TestNewSourceUsecase(t *testing.T) { 22 | type fields struct { 23 | sourceRepository repository.SourceRepositoryInterface 24 | fetcherUtil utils.FetcherUtilInterface 25 | } 26 | 27 | tests := []struct { 28 | name string 29 | fields fields 30 | want *SourceUsecase 31 | }{ 32 | { 33 | name: "Success", 34 | fields: fields{ 35 | sourceRepository: &mockSourceRepository{}, 36 | fetcherUtil: &mockFetcherUtil{}, 37 | }, 38 | want: &SourceUsecase{ 39 | SourceRepository: &mockSourceRepository{}, 40 | FetcherUtil: &mockFetcherUtil{}, 41 | }, 42 | }, 43 | } 44 | 45 | for _, tt := range tests { 46 | t.Run(tt.name, func(t *testing.T) { 47 | sourceUsecase := NewSourceUsecase(tt.fields.sourceRepository, tt.fields.fetcherUtil) 48 | if sourceUsecase == nil { 49 | t.Errorf(expectedReturnNonNil, "NewSourceUsecase", "SourceUsecaseInterface") 50 | } 51 | 52 | got, ok := sourceUsecase.(*SourceUsecase) 53 | if !ok { 54 | t.Errorf(expectedTypeAssertionErrorMessage, "*SourceUsecase") 55 | } 56 | 57 | if !reflect.DeepEqual(tt.want, got) { 58 | t.Errorf(expectedButGotMessage, "*SourceUsecase", tt.want, got) 59 | } 60 | }) 61 | } 62 | } 63 | 64 | func TestLoadSourcesSuccess(t *testing.T) { 65 | type fields struct { 66 | sourceRepository repository.SourceRepositoryInterface 67 | } 68 | 69 | tests := []struct { 70 | name string 71 | fields fields 72 | want []entity.Source 73 | wantError error 74 | }{ 75 | { 76 | name: "Success", 77 | fields: fields{ 78 | sourceRepository: &mockSourceRepository{ 79 | LoadSourcesFunc: func() ([]entity.Source, error) { 80 | return []entity.Source{ 81 | { 82 | Method: testListMethod, 83 | Category: testCategory, 84 | URL: testURL, 85 | IsChecked: testIsChecked, 86 | }, 87 | }, nil 88 | }, 89 | }, 90 | }, 91 | want: []entity.Source{ 92 | { 93 | Method: testListMethod, 94 | Category: testCategory, 95 | URL: testURL, 96 | IsChecked: testIsChecked, 97 | }, 98 | }, 99 | wantError: nil, 100 | }, 101 | { 102 | name: "Error", 103 | fields: fields{ 104 | sourceRepository: &mockSourceRepository{ 105 | LoadSourcesFunc: func() ([]entity.Source, error) { 106 | return nil, errors.New("load proxy resource error") 107 | }, 108 | }, 109 | }, 110 | want: nil, 111 | wantError: errors.New("load proxy resource error"), 112 | }, 113 | } 114 | 115 | for _, tt := range tests { 116 | t.Run(tt.name, func(t *testing.T) { 117 | uc := &SourceUsecase{ 118 | SourceRepository: tt.fields.sourceRepository, 119 | } 120 | got, err := uc.LoadSources() 121 | 122 | if err != nil && err.Error() != tt.wantError.Error() { 123 | t.Errorf(expectedErrorButGotMessage, "ProcessProxy()", tt.wantError, err) 124 | } 125 | 126 | if !reflect.DeepEqual(got, tt.want) { 127 | t.Errorf(expectedButGotMessage, "ProcessProxy()", tt.want, got) 128 | } 129 | }) 130 | } 131 | } 132 | 133 | func TestProcessSource(t *testing.T) { 134 | type fields struct { 135 | fetcherUtil utils.FetcherUtilInterface 136 | } 137 | 138 | type args struct { 139 | source entity.Source 140 | } 141 | 142 | tests := []struct { 143 | name string 144 | fields fields 145 | args args 146 | want []string 147 | wantError error 148 | }{ 149 | { 150 | name: "TestFetcherError", 151 | fields: fields{ 152 | fetcherUtil: &mockFetcherUtil{ 153 | fetcherError: errors.New("error creating request"), 154 | }, 155 | }, 156 | args: args{ 157 | source: entity.Source{ 158 | Method: testListMethod, 159 | Category: testCategory, 160 | URL: testURL, 161 | IsChecked: testIsChecked, 162 | }, 163 | }, 164 | want: nil, 165 | wantError: errors.New("error creating request"), 166 | }, 167 | { 168 | name: "TestFetcherWithListMethod", 169 | fields: fields{ 170 | fetcherUtil: &mockFetcherUtil{ 171 | fetchDataByte: []byte(testProxy1 + "\n" + testProxy2 + "\n" + testProxy3 + "\n" + testProxy4), 172 | }, 173 | }, 174 | args: args{ 175 | source: entity.Source{ 176 | Method: testListMethod, 177 | Category: testCategory, 178 | URL: testURL, 179 | IsChecked: testIsChecked, 180 | }, 181 | }, 182 | want: []string{ 183 | testProxy1, 184 | testProxy2, 185 | testProxy3, 186 | testProxy4, 187 | }, 188 | wantError: nil, 189 | }, 190 | { 191 | name: "TestFetcherWithScrapMethod", 192 | fields: fields{ 193 | fetcherUtil: &mockFetcherUtil{ 194 | fetchDataByte: []byte(testProxy1 + "\n" + testProxy2 + "\n" + testProxy3 + "\n" + testProxy4), 195 | }, 196 | }, 197 | args: args{ 198 | source: entity.Source{ 199 | Method: testScrapMethod, 200 | Category: testCategory, 201 | URL: testURL, 202 | IsChecked: testIsChecked, 203 | }, 204 | }, 205 | want: []string{ 206 | testProxy1, 207 | testProxy2, 208 | testProxy3, 209 | testProxy4, 210 | }, 211 | wantError: nil, 212 | }, 213 | { 214 | name: "TestFetcherWithUndefinedMethod", 215 | fields: fields{ 216 | fetcherUtil: &mockFetcherUtil{}, 217 | }, 218 | args: args{ 219 | source: entity.Source{ 220 | Method: "NO_METHOD", 221 | Category: testCategory, 222 | URL: testURL, 223 | IsChecked: testIsChecked, 224 | }, 225 | }, 226 | want: nil, 227 | wantError: errors.New("source method not found: NO_METHOD"), 228 | }, 229 | } 230 | 231 | for _, tt := range tests { 232 | t.Run(tt.name, func(t *testing.T) { 233 | uc := &SourceUsecase{ 234 | FetcherUtil: tt.fields.fetcherUtil, 235 | } 236 | got, err := uc.ProcessSource(&tt.args.source) 237 | 238 | if (err != nil && tt.wantError != nil && err.Error() != tt.wantError.Error()) || 239 | (err == nil && tt.wantError != nil) || 240 | (err != nil && tt.wantError == nil) { 241 | t.Errorf(expectedErrorButGotMessage, "SourceUsecase.ProcessSource()", tt.wantError, err) 242 | } 243 | 244 | if !reflect.DeepEqual(got, tt.want) { 245 | t.Errorf(expectedButGotMessage, "SourceUsecase.ProcessSource()", tt.want, got) 246 | } 247 | }) 248 | } 249 | } 250 | -------------------------------------------------------------------------------- /internal/infrastructure/repository/file_repository.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | import ( 4 | "encoding/json" 5 | "encoding/xml" 6 | "fmt" 7 | "io" 8 | "io/fs" 9 | "os" 10 | "path/filepath" 11 | "strings" 12 | 13 | "github.com/fyvri/fresh-proxy-list/internal/entity" 14 | "github.com/fyvri/fresh-proxy-list/pkg/utils" 15 | 16 | "gopkg.in/yaml.v3" 17 | ) 18 | 19 | type FileRepository struct { 20 | MkdirAll func(path string, perm os.FileMode) error 21 | Create func(name string) (io.Writer, error) 22 | CSVWriter utils.CSVWriterUtilInterface 23 | } 24 | 25 | type FileRepositoryInterface interface { 26 | SaveFile(filePath string, data interface{}, format string) error 27 | CreateDirectory(filePath string) error 28 | WriteTxt(writer io.Writer, data interface{}) error 29 | EncodeCSV(writer io.Writer, data interface{}) error 30 | WriteCSV(writer io.Writer, header []string, rows [][]string) error 31 | EncodeJSON(writer io.Writer, data interface{}) error 32 | EncodeXML(writer io.Writer, data interface{}) error 33 | EncodeYAML(writer io.Writer, data interface{}) error 34 | } 35 | 36 | type MkdirAllFunc func(path string, perm os.FileMode) error 37 | type CreateFunc func(name string) (io.Writer, error) 38 | 39 | func NewFileRepository(mkdirAll MkdirAllFunc, create CreateFunc, csvWriter utils.CSVWriterUtilInterface) FileRepositoryInterface { 40 | return &FileRepository{ 41 | MkdirAll: mkdirAll, 42 | Create: create, 43 | CSVWriter: csvWriter, 44 | } 45 | } 46 | 47 | func (r *FileRepository) SaveFile(filePath string, data interface{}, format string) error { 48 | if err := r.CreateDirectory(filePath); err != nil { 49 | return err 50 | } 51 | 52 | file, err := r.Create(filePath) 53 | if err != nil { 54 | return fmt.Errorf("error creating file %s: %v", filePath, err) 55 | } 56 | defer func() { 57 | if f, ok := file.(io.Closer); ok { 58 | f.Close() 59 | } 60 | }() 61 | 62 | switch format { 63 | case "txt": 64 | return r.WriteTxt(file, data) 65 | case "json": 66 | return r.EncodeJSON(file, data) 67 | case "csv": 68 | return r.EncodeCSV(file, data) 69 | case "xml": 70 | return r.EncodeXML(file, data) 71 | case "yaml": 72 | return r.EncodeYAML(file, data) 73 | default: 74 | return fmt.Errorf("unsupported format: %s", format) 75 | } 76 | } 77 | 78 | func (r *FileRepository) CreateDirectory(filePath string) error { 79 | err := r.MkdirAll(filepath.Dir(filePath), fs.ModePerm) 80 | if err != nil { 81 | return fmt.Errorf("error creating directory %s: %v", filePath, err) 82 | } 83 | return nil 84 | } 85 | 86 | func (r *FileRepository) WriteTxt(writer io.Writer, data interface{}) error { 87 | var dataString string 88 | if stringData, ok := data.([]string); ok { 89 | dataString = strings.Join(stringData, "\n") 90 | } 91 | 92 | _, err := writer.Write([]byte(dataString)) 93 | if err != nil { 94 | return fmt.Errorf("error writing TXT: %v", err) 95 | } 96 | return nil 97 | } 98 | 99 | func (r *FileRepository) EncodeCSV(writer io.Writer, data interface{}) error { 100 | switch proxyData := data.(type) { 101 | case []string: 102 | rows := make([][]string, len(proxyData)) 103 | for i, rowElem := range proxyData { 104 | rows[i] = []string{rowElem} 105 | } 106 | return r.WriteCSV(writer, nil, rows) 107 | case []entity.Proxy: 108 | header := []string{"Proxy", "IP", "Port", "TimeTaken", "CheckedAt"} 109 | rows := make([][]string, len(proxyData)) 110 | for i, proxy := range proxyData { 111 | rows[i] = []string{proxy.Proxy, proxy.IP, proxy.Port, fmt.Sprintf("%v", proxy.TimeTaken), proxy.CheckedAt} 112 | } 113 | return r.WriteCSV(writer, header, rows) 114 | case []entity.AdvancedProxy: 115 | header := []string{"Proxy", "IP", "Port", "Categories", "TimeTaken", "CheckedAt"} 116 | rows := make([][]string, len(proxyData)) 117 | for i, proxy := range proxyData { 118 | rows[i] = []string{proxy.Proxy, proxy.IP, proxy.Port, strings.Join(proxy.Categories, ","), fmt.Sprintf("%v", proxy.TimeTaken), proxy.CheckedAt} 119 | } 120 | return r.WriteCSV(writer, header, rows) 121 | default: 122 | return fmt.Errorf("invalid data type for CSV encoding") 123 | } 124 | } 125 | 126 | func (r *FileRepository) WriteCSV(writer io.Writer, header []string, rows [][]string) error { 127 | csvWriter := r.CSVWriter.Init(writer) 128 | defer r.CSVWriter.Flush(csvWriter) 129 | 130 | if header != nil { 131 | if err := r.CSVWriter.Write(csvWriter, header); err != nil { 132 | return fmt.Errorf("failed to write header: %w", err) 133 | } 134 | } 135 | 136 | for _, row := range rows { 137 | if err := r.CSVWriter.Write(csvWriter, row); err != nil { 138 | return fmt.Errorf("failed to write row: %w", err) 139 | } 140 | } 141 | 142 | return nil 143 | } 144 | 145 | func (r *FileRepository) EncodeJSON(writer io.Writer, data interface{}) error { 146 | err := json.NewEncoder(writer).Encode(data) 147 | if err != nil { 148 | return fmt.Errorf("error encoding JSON: %v", err) 149 | } 150 | return nil 151 | } 152 | 153 | func (r *FileRepository) EncodeXML(writer io.Writer, data interface{}) error { 154 | var err error 155 | switch proxyData := data.(type) { 156 | case []string: 157 | view := entity.ProxyXMLClassicView{ 158 | XMLName: xml.Name{Local: "proxies"}, 159 | Proxies: make([]string, len(proxyData)), 160 | } 161 | copy(view.Proxies, proxyData) 162 | err = xml.NewEncoder(writer).Encode(view) 163 | case []entity.Proxy: 164 | view := entity.ProxyXMLAdvancedView{ 165 | XMLName: xml.Name{Local: "proxies"}, 166 | Proxies: make([]entity.Proxy, len(proxyData)), 167 | } 168 | copy(view.Proxies, proxyData) 169 | err = xml.NewEncoder(writer).Encode(view) 170 | case []entity.AdvancedProxy: 171 | view := entity.ProxyXMLAllAdvancedView{ 172 | XMLName: xml.Name{Local: "Proxies"}, 173 | Proxies: make([]entity.AdvancedProxy, len(proxyData)), 174 | } 175 | copy(view.Proxies, proxyData) 176 | err = xml.NewEncoder(writer).Encode(view) 177 | } 178 | 179 | if err != nil { 180 | return fmt.Errorf("error encoding XML: %v", err) 181 | } 182 | return nil 183 | } 184 | 185 | func (r *FileRepository) EncodeYAML(writer io.Writer, data interface{}) error { 186 | var err error 187 | switch proxyData := data.(type) { 188 | case []string: 189 | view := struct { 190 | Proxies []string `yaml:"proxies"` 191 | }{ 192 | Proxies: make([]string, len(proxyData)), 193 | } 194 | copy(view.Proxies, proxyData) 195 | err = yaml.NewEncoder(writer).Encode(view) 196 | case []entity.Proxy: 197 | view := struct { 198 | Proxies []entity.Proxy `yaml:"proxies"` 199 | }{ 200 | Proxies: make([]entity.Proxy, len(proxyData)), 201 | } 202 | copy(view.Proxies, proxyData) 203 | err = yaml.NewEncoder(writer).Encode(view) 204 | case []entity.AdvancedProxy: 205 | view := struct { 206 | Proxies []entity.AdvancedProxy `yaml:"proxies"` 207 | }{ 208 | Proxies: make([]entity.AdvancedProxy, len(proxyData)), 209 | } 210 | copy(view.Proxies, proxyData) 211 | err = yaml.NewEncoder(writer).Encode(view) 212 | } 213 | 214 | if err != nil { 215 | return fmt.Errorf("error encoding YAML: %v", err) 216 | } 217 | return nil 218 | } 219 | -------------------------------------------------------------------------------- /.github/workflows/continuous-integration.yml: -------------------------------------------------------------------------------- 1 | name: Continuous Integration 2 | 3 | on: 4 | schedule: 5 | - cron: "0 * * * *" 6 | workflow_dispatch: 7 | inputs: 8 | logLevel: 9 | description: "Log level" 10 | required: true 11 | default: "info" 12 | type: choice 13 | options: 14 | - info 15 | - warning 16 | - debug 17 | 18 | env: 19 | TZ: Asia/Jakarta 20 | EMOJI_CHEAT_SHEETS: "🤯,👻,😻,💕,🤍,💨,🦸,🧚,🧜‍♀️,🧞,💃,🦍,🐅,🦄,🐏,🦙,🦣,🦥,🦦,🐔,🐣,🕊️,🐉,🦕,🦖,🐳,🐬,🦭,🦋,🦠,🌻,🌼,🌱,🌿,🍀,🍃,🍻,🛫,🪂,🚀,🛸,🌟,⚡,🔥,✨,🎉,🧬" 21 | 22 | jobs: 23 | build: 24 | name: Build and Testing 25 | runs-on: ubuntu-latest 26 | 27 | steps: 28 | - name: Checkout code 29 | uses: actions/checkout@v4 30 | 31 | - name: Set up Go 32 | uses: actions/setup-go@v5 33 | with: 34 | go-version-file: "go.mod" 35 | 36 | - name: Build 37 | run: go build -v ./... 38 | 39 | - name: Test 40 | run: go test -v ./... 41 | 42 | prepare: 43 | name: Prepare Branch 44 | runs-on: ubuntu-latest 45 | needs: [build] 46 | outputs: 47 | emoji: ${{ steps.select-emoji.outputs.emoji }} 48 | 49 | steps: 50 | - name: Checkout main branch 51 | uses: actions/checkout@v4 52 | with: 53 | ref: main 54 | 55 | - name: Remove ${{ vars.ARCHIVE_BRANCH_NAME }} branch if it exists 56 | run: | 57 | if git ls-remote --exit-code --heads origin ${{ vars.ARCHIVE_BRANCH_NAME }}; then 58 | echo "Branch deleted successfully" 59 | git push https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }} --delete ${{ vars.ARCHIVE_BRANCH_NAME }} 60 | else 61 | echo "Branch does not exist" 62 | fi 63 | 64 | - name: Select an emoji 65 | id: select-emoji 66 | run: | 67 | echo "EMOJI=$(echo $EMOJI_CHEAT_SHEETS | tr ',' '\n' | shuf -n 1)" >> $GITHUB_OUTPUT 68 | 69 | update: 70 | name: Update Proxies 71 | runs-on: ubuntu-latest 72 | needs: [prepare] 73 | permissions: 74 | contents: write 75 | 76 | steps: 77 | - name: Checkout main branch 78 | uses: actions/checkout@v4 79 | with: 80 | ref: main 81 | 82 | - name: Configure GIT 83 | run: | 84 | git config --global user.name "$(git log --reverse --format='%an' | head -n 1)" 85 | git config --global user.email "$(git log --reverse --format='%ae' | head -n 1)" 86 | 87 | - name: Create ${{ vars.ARCHIVE_BRANCH_NAME }} branch 88 | run: | 89 | git checkout -b ${{ vars.ARCHIVE_BRANCH_NAME }} 90 | 91 | - name: Set up Go 92 | uses: actions/setup-go@v5 93 | with: 94 | go-version-file: "go.mod" 95 | 96 | - name: Install dependencies 97 | run: | 98 | go mod tidy 99 | 100 | - name: Run Go program 101 | env: 102 | PROXY_RESOURCES: ${{ secrets.PROXY_RESOURCES }} 103 | run: | 104 | go run ./cmd/main.go 105 | 106 | - name: Check for changes 107 | run: | 108 | if [ "$(git status --porcelain storage)" ]; then 109 | echo "Changes detected" 110 | echo "CHANGES_EXIST=true" >> $GITHUB_ENV 111 | else 112 | echo "No changes to commit" 113 | echo "CHANGES_EXIST=false" >> $GITHUB_ENV 114 | fi 115 | 116 | - name: Commit files 117 | if: env.CHANGES_EXIST == 'true' 118 | run: | 119 | git add storage 120 | git commit -m "chore(bot): update proxies at $(date '+%a, %d %b %Y %H:%M:%S (GMT+07:00)' | tr '[:upper:]' '[:lower:]') ${{ needs.prepare.outputs.emoji }}" 121 | 122 | - name: Push changes to ${{ vars.ARCHIVE_BRANCH_NAME }} branch 123 | if: env.CHANGES_EXIST == 'true' 124 | uses: ad-m/github-push-action@master 125 | with: 126 | github_token: ${{ secrets.GITHUB_TOKEN }} 127 | branch: ${{ vars.ARCHIVE_BRANCH_NAME }} 128 | force: true 129 | 130 | release: 131 | name: Release 132 | runs-on: ubuntu-latest 133 | needs: [prepare, update] 134 | permissions: 135 | contents: write 136 | 137 | steps: 138 | - name: Checkout ${{ vars.ARCHIVE_BRANCH_NAME }} branch 139 | uses: actions/checkout@v4 140 | with: 141 | ref: ${{ vars.ARCHIVE_BRANCH_NAME }} 142 | 143 | - name: Configure GIT 144 | run: | 145 | git config --global user.name "$(git log --reverse --format='%an' | head -n 1)" 146 | git config --global user.email "$(git log --reverse --format='%ae' | head -n 1)" 147 | 148 | - name: Get last commit time on ${{ vars.ARCHIVE_BRANCH_NAME }} branch 149 | run: | 150 | echo "UPDATED_AT=$(date -d "$(git log -1 --format=%cd --date=iso-strict)" "+%A, %B %e, %Y at %H:%M:%S (GMT+07:00)" | awk '{$1=$1; print}')" >> $GITHUB_ENV 151 | 152 | - name: Count proxies 153 | run: | 154 | echo "HTTP_PROXY_COUNT=$(grep -v '^$' ./storage/classic/http.txt | wc -l)" >> $GITHUB_ENV 155 | echo "HTTPS_PROXY_COUNT=$(grep -v '^$' ./storage/classic/https.txt | wc -l)" >> $GITHUB_ENV 156 | echo "SOCKS4_PROXY_COUNT=$(grep -v '^$' ./storage/classic/socks4.txt | wc -l)" >> $GITHUB_ENV 157 | echo "SOCKS5_PROXY_COUNT=$(grep -v '^$' ./storage/classic/socks5.txt | wc -l)" >> $GITHUB_ENV 158 | 159 | - name: Extract 10 fresh proxies 160 | run: | 161 | echo "HTTP_PROXIES=$(shuf -n 10 ./storage/classic/http.txt | awk '{printf "%s\\n", $0}')" >> $GITHUB_ENV 162 | echo "HTTPS_PROXIES=$(shuf -n 10 ./storage/classic/https.txt | awk '{printf "%s\\n", $0}')" >> $GITHUB_ENV 163 | echo "SOCKS4_PROXIES=$(shuf -n 10 ./storage/classic/socks4.txt | awk '{printf "%s\\n", $0}')" >> $GITHUB_ENV 164 | echo "SOCKS5_PROXIES=$(shuf -n 10 ./storage/classic/socks5.txt | awk '{printf "%s\\n", $0}')" >> $GITHUB_ENV 165 | 166 | - name: Checkout main branch 167 | uses: actions/checkout@v4 168 | with: 169 | ref: main 170 | 171 | - name: Update documentation 172 | run: | 173 | sed \ 174 | -e "s/{{UPDATED_AT}}/${UPDATED_AT}/" \ 175 | -e "s/{{HTTP_PROXY_COUNT}}/${HTTP_PROXY_COUNT}/" \ 176 | -e "s/{{HTTPS_PROXY_COUNT}}/${HTTPS_PROXY_COUNT}/" \ 177 | -e "s/{{SOCKS4_PROXY_COUNT}}/${SOCKS4_PROXY_COUNT}/" \ 178 | -e "s/{{SOCKS5_PROXY_COUNT}}/${SOCKS5_PROXY_COUNT}/" \ 179 | -e "s/{{HTTP_PROXIES}}/${HTTP_PROXIES}/" \ 180 | -e "s/{{HTTPS_PROXIES}}/${HTTPS_PROXIES}/" \ 181 | -e "s/{{SOCKS4_PROXIES}}/${SOCKS4_PROXIES}/" \ 182 | -e "s/{{SOCKS5_PROXIES}}/${SOCKS5_PROXIES}/" \ 183 | ./docs/README.template.md > ./README.md 184 | 185 | - name: Check for changes 186 | run: | 187 | if [ "$(git status --porcelain README.md)" ]; then 188 | echo "Changes detected" 189 | echo "CHANGES_EXIST=true" >> $GITHUB_ENV 190 | else 191 | echo "No changes to commit" 192 | echo "CHANGES_EXIST=false" >> $GITHUB_ENV 193 | fi 194 | 195 | - name: Commit changes 196 | if: env.CHANGES_EXIST == 'true' 197 | run: | 198 | git add README.md 199 | git commit -m "docs: release fresh proxy list ${{ needs.prepare.outputs.emoji }}" 200 | 201 | - name: Push changes 202 | if: env.CHANGES_EXIST == 'true' 203 | uses: ad-m/github-push-action@master 204 | with: 205 | github_token: ${{ secrets.GITHUB_TOKEN }} 206 | branch: main 207 | force: true 208 | -------------------------------------------------------------------------------- /docs/README.template.md: -------------------------------------------------------------------------------- 1 | [donate::shield]: https://img.shields.io/badge/Donate-PayPal-0070BA?logo=paypal 2 | [donate::url]: https://paypal.me/membasuh 3 | [contributors::shield]: https://img.shields.io/github/contributors/fyvri/fresh-proxy-list?style=flat 4 | [contributors::url]: https://github.com/fyvri/fresh-proxy-list/graphs/contributors 5 | [license::shield]: https://img.shields.io/badge/License-MIT-4b9081?style=flat 6 | [license::url]: https://github.com/fyvri/fresh-proxy-list/blob/HEAD/LICENSE.md 7 | [watchers::shield]: https://img.shields.io/github/watchers/fyvri/fresh-proxy-list?style=flat&logo=github&label=Watchers 8 | [watchers::url]: https://github.com/fyvri/fresh-proxy-list/watchers 9 | [stars::shield]: https://img.shields.io/github/stars/fyvri/fresh-proxy-list?style=flat&logo=github&label=Stars 10 | [stars::url]: https://github.com/fyvri/fresh-proxy-list/stargazers 11 | [forks::shield]: https://img.shields.io/github/forks/fyvri/fresh-proxy-list?style=flat&logo=github&label=Forks 12 | [forks::url]: https://github.com/fyvri/fresh-proxy-list/network/members 13 | [continuous-integration::shield]: https://github.com/fyvri/fresh-proxy-list/actions/workflows/continuous-integration.yml/badge.svg 14 | [continuous-integration::url]: https://github.com/fyvri/fresh-proxy-list/actions/workflows/continuous-integration.yml 15 | [static-analysis::shield]: https://github.com/fyvri/fresh-proxy-list/actions/workflows/static-analysis.yml/badge.svg 16 | [static-analysis::url]: https://github.com/fyvri/fresh-proxy-list/actions/workflows/static-analysis.yml 17 | [last-commit::shield]: https://img.shields.io/github/last-commit/fyvri/fresh-proxy-list?style=flat&logo=github&label=last+update 18 | [last-commit::url]: https://github.com/fyvri/fresh-proxy-list/activity?ref=archive&activity_type=branch_creation 19 | [commit-activity::shield]: https://img.shields.io/github/commit-activity/w/fyvri/fresh-proxy-list?style=flat&logo=github 20 | [commit-activity::url]: https://github.com/fyvri/fresh-proxy-list/commits/main 21 | [discussions::shield]: https://img.shields.io/github/discussions/fyvri/fresh-proxy-list?style=flat&logo=github 22 | [discussions::url]: https://github.com/fyvri/fresh-proxy-list/discussions 23 | [issues::shield]: https://img.shields.io/github/issues/fyvri/fresh-proxy-list?style=flat&logo=github 24 | [issues::url]: https://github.com/fyvri/fresh-proxy-list/issues 25 | 26 |
27 | 28 |

Fresh Proxy List

29 | 30 | [![Donate][donate::shield]][donate::url] 31 | [![License][license::shield]][license::url] 32 | [![Static Analysis][static-analysis::shield]][static-analysis::url] 33 | [![Continuous Integration][continuous-integration::shield]][continuous-integration::url] 34 |
35 | [![Last Commit][last-commit::shield]][last-commit::url] 36 | [![Commit Activity][commit-activity::shield]][commit-activity::url] 37 | [![Discussions][discussions::shield]][discussions::url] 38 | [![Issues][issues::shield]][issues::url] 39 | 40 | An automatically ⏰ updated list of free `HTTP`, `HTTPS`, `SOCKS4`, and `SOCKS5` proxies, available in multiple formats including `TXT`, `CSV`, `JSON`, `XML`, and `YAML`. The list is refreshed ⚡ **hourly** to provide the most accurate 🎯 and up-to-date information. The current data snapshot was 🚀 last updated on `{{UPDATED_AT}}`, ensuring that users have access to the latest and most reliable proxies 🍃 available. 41 | 42 | 43 | HTTP 44 | 45 |   46 | 47 | HTTPS 48 | 49 |   50 | 51 | SOCKS4 52 | 53 |   54 | 55 | SOCKS5 56 | 57 | 58 |
59 | 60 | ## 📃 About 61 | 62 | This repository contains a free list of `HTTP/S` and `SOCKS4/5` proxies. 63 | 64 | - [x] 24/7 hourly updates (Committing since December 2024) 65 | - [x] Supported list formats: `TXT`, `CSV`, `JSON`, `XML`, and `YAML` 66 | - [x] No authentication is required when connecting any of these proxies 67 | 68 | > [!TIP] 69 | > Be sure to read this documentation. 70 | 71 |

[ back to top ]

72 | 73 | ## 🔗 Proxy List Links 74 | 75 | Duplicated proxies are removed — the only exception is if an IP has a different port open. 76 | 77 | 1. Classic View (IP:Port only) 78 | 79 | | Category | Links by File Type | 80 | | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 81 | | All | [`TXT`](https://raw.githubusercontent.com/fyvri/fresh-proxy-list/archive/storage/classic/all.txt), [`CSV`](https://raw.githubusercontent.com/fyvri/fresh-proxy-list/archive/storage/classic/all.csv), [`JSON`](https://raw.githubusercontent.com/fyvri/fresh-proxy-list/archive/storage/classic/all.json), [`XML`](https://raw.githubusercontent.com/fyvri/fresh-proxy-list/archive/storage/classic/all.xml), [`YAML`](https://raw.githubusercontent.com/fyvri/fresh-proxy-list/archive/storage/classic/all.yaml) | 82 | | HTTP | [`TXT`](https://raw.githubusercontent.com/fyvri/fresh-proxy-list/archive/storage/classic/http.txt), [`CSV`](https://raw.githubusercontent.com/fyvri/fresh-proxy-list/archive/storage/classic/http.csv), [`JSON`](https://raw.githubusercontent.com/fyvri/fresh-proxy-list/archive/storage/classic/http.json), [`XML`](https://raw.githubusercontent.com/fyvri/fresh-proxy-list/archive/storage/classic/http.xml), [`YAML`](https://raw.githubusercontent.com/fyvri/fresh-proxy-list/archive/storage/classic/http.yaml) | 83 | | HTTPS | [`TXT`](https://raw.githubusercontent.com/fyvri/fresh-proxy-list/archive/storage/classic/https.txt), [`CSV`](https://raw.githubusercontent.com/fyvri/fresh-proxy-list/archive/storage/classic/https.csv), [`JSON`](https://raw.githubusercontent.com/fyvri/fresh-proxy-list/archive/storage/classic/https.json), [`XML`](https://raw.githubusercontent.com/fyvri/fresh-proxy-list/archive/storage/classic/https.xml), [`YAML`](https://raw.githubusercontent.com/fyvri/fresh-proxy-list/archive/storage/classic/https.yaml) | 84 | | SOCKS4 | [`TXT`](https://raw.githubusercontent.com/fyvri/fresh-proxy-list/archive/storage/classic/socks4.txt), [`CSV`](https://raw.githubusercontent.com/fyvri/fresh-proxy-list/archive/storage/classic/socks4.csv), [`JSON`](https://raw.githubusercontent.com/fyvri/fresh-proxy-list/archive/storage/classic/socks4.json), [`XML`](https://raw.githubusercontent.com/fyvri/fresh-proxy-list/archive/storage/classic/socks4.xml), [`YAML`](https://raw.githubusercontent.com/fyvri/fresh-proxy-list/archive/storage/classic/socks4.yaml) | 85 | | SOCKS5 | [`TXT`](https://raw.githubusercontent.com/fyvri/fresh-proxy-list/archive/storage/classic/socks5.txt), [`CSV`](https://raw.githubusercontent.com/fyvri/fresh-proxy-list/archive/storage/classic/socks5.csv), [`JSON`](https://raw.githubusercontent.com/fyvri/fresh-proxy-list/archive/storage/classic/socks5.json), [`XML`](https://raw.githubusercontent.com/fyvri/fresh-proxy-list/archive/storage/classic/socks5.xml), [`YAML`](https://raw.githubusercontent.com/fyvri/fresh-proxy-list/archive/storage/classic/socks5.yaml) | 86 | 87 | 2. Advanced View (With full information) 88 | 89 | _Coming soon_ 90 | 91 |

[ back to top ]

92 | 93 | ## 🎁 Example Results 94 | 95 | HTTP 96 | 97 | ```txt 98 | {{HTTP_PROXIES}} 99 | ``` 100 | 101 | HTTPS 102 | 103 | ```txt 104 | {{HTTPS_PROXIES}} 105 | ``` 106 | 107 | SOCKS4 108 | 109 | ```txt 110 | {{SOCKS4_PROXIES}} 111 | ``` 112 | 113 | SOCKS5 114 | 115 | ```txt 116 | {{SOCKS5_PROXIES}} 117 | ``` 118 | 119 |

[ back to top ]

120 | 121 | ## 👥 Contributing 122 | 123 | If you have any ideas, [open an issue](https://github.com/fyvri/fresh-proxy-list/issues/new) and tell me what you think. 124 | 125 | Contributions are what make the open-source community such an amazing place to learn, inspire, and create. Any contributions you make are **greatly appreciated**. 126 | 127 | > [!IMPORTANT] 128 | > If you have a suggestion that would make this better, please fork the repo and create a pull request. Don't forget to give the project a star 🌟 I can't stop saying thank you! 129 | > 130 | > 1. Fork this project 131 | > 2. Create your feature branch (`git checkout -b feature/awesome-feature`) 132 | > 3. Commit your changes (`git commit -m "feat: add awesome feature"`) 133 | > 4. Push to the branch (`git push origin feature/awesome-feature`) 134 | > 5. Open a pull request 135 | 136 |

[ back to top ]

137 | 138 | ## 📜 License 139 | 140 | This project is licensed under the [MIT License](LICENSE). Feel free to use and modify it as needed. 141 | 142 |

[ back to top ]

143 | -------------------------------------------------------------------------------- /internal/service/proxy_service_test.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "io" 7 | "net/http" 8 | "net/http/httptest" 9 | "net/url" 10 | "reflect" 11 | "testing" 12 | "time" 13 | 14 | "github.com/fyvri/fresh-proxy-list/internal/entity" 15 | "github.com/fyvri/fresh-proxy-list/pkg/utils" 16 | ) 17 | 18 | var ( 19 | expectedButGotMessage = "Expected %v = %v, but got = %v" 20 | expectedErrorButGotMessage = "Expected %v error = %v, but got = %v" 21 | expectedNonEmptyMessage = "Expected non-empty %v from %v" 22 | expectedReturnNonNil = "Expected %v to return a non-nil %v" 23 | expectedTypeAssertionErrorMessage = "Expected type assertion error, but got = %v" 24 | testIP = "13.37.0.1" 25 | testPort = "8080" 26 | testProxy = testIP + ":" + testPort 27 | testHTTPCategory = "HTTP" 28 | testHTTPSCategory = "HTTPS" 29 | testSOCKS4Category = "SOCKS4" 30 | testHTTPTestingSites = []string{"http://test1.com", "http://test2.com"} 31 | testHTTPSTestingSites = []string{"https://secure1.com", "https://secure2.com"} 32 | testUserAgents = []string{"Mozilla", "Chrome", "Safari"} 33 | ) 34 | 35 | type mockURLParserUtil struct { 36 | ParseFunc func(urlStr string) (*url.URL, error) 37 | } 38 | 39 | func (m *mockURLParserUtil) Parse(urlStr string) (*url.URL, error) { 40 | if m.ParseFunc != nil { 41 | return m.ParseFunc(urlStr) 42 | } 43 | return url.Parse(urlStr) 44 | } 45 | 46 | type mockFetcherUtil struct { 47 | fetchDataByte []byte 48 | fetcherError error 49 | NewRequestFunc func(method, url string, body io.Reader) (*http.Request, error) 50 | DoFunc func(client *http.Client, req *http.Request) (*http.Response, error) 51 | } 52 | 53 | func (m *mockFetcherUtil) FetchData(url string) ([]byte, error) { 54 | if m.fetcherError != nil { 55 | return nil, m.fetcherError 56 | } 57 | return m.fetchDataByte, nil 58 | } 59 | 60 | func (m *mockFetcherUtil) Do(client *http.Client, req *http.Request) (*http.Response, error) { 61 | if m.DoFunc != nil { 62 | return m.DoFunc(client, req) 63 | } 64 | return httptest.NewRecorder().Result(), nil 65 | } 66 | 67 | func (m *mockFetcherUtil) NewRequest(method string, url string, body io.Reader) (*http.Request, error) { 68 | if m.NewRequestFunc != nil { 69 | return m.NewRequestFunc(method, url, body) 70 | } 71 | return http.NewRequest(method, url, body) 72 | } 73 | 74 | func TestNewProxyService(t *testing.T) { 75 | proxyService := NewProxyService(&mockFetcherUtil{}, &mockURLParserUtil{}, testHTTPTestingSites, testHTTPSTestingSites, testUserAgents) 76 | if proxyService == nil { 77 | t.Errorf(expectedReturnNonNil, "NewProxyService", "ProxyServiceInterface") 78 | } 79 | 80 | s, ok := proxyService.(*ProxyService) 81 | if !ok { 82 | t.Errorf(expectedTypeAssertionErrorMessage, "*ProxyService") 83 | } 84 | 85 | if !reflect.DeepEqual(s.HTTPTestingSites, testHTTPTestingSites) { 86 | t.Errorf(expectedButGotMessage, "HTTPTestingSites", testHTTPTestingSites, s.HTTPTestingSites) 87 | } 88 | 89 | if !reflect.DeepEqual(s.HTTPSTestingSites, testHTTPSTestingSites) { 90 | t.Errorf(expectedButGotMessage, "HTTPSTestingSites", testHTTPSTestingSites, s.HTTPSTestingSites) 91 | } 92 | 93 | if !reflect.DeepEqual(s.UserAgents, testUserAgents) { 94 | t.Errorf(expectedButGotMessage, "UserAgents", testUserAgents, s.UserAgents) 95 | } 96 | } 97 | 98 | func TestCheck(t *testing.T) { 99 | type fields struct { 100 | fetcherUtil utils.FetcherUtilInterface 101 | urlParserUtil utils.URLParserUtilInterface 102 | } 103 | 104 | type args struct { 105 | category string 106 | ip string 107 | port string 108 | } 109 | 110 | tests := []struct { 111 | name string 112 | fields fields 113 | args args 114 | want *entity.Proxy 115 | wantError error 116 | }{ 117 | { 118 | name: "TestValid", 119 | fields: fields{ 120 | fetcherUtil: &mockFetcherUtil{}, 121 | urlParserUtil: &mockURLParserUtil{}, 122 | }, 123 | args: args{ 124 | category: testHTTPSCategory, 125 | ip: testIP, 126 | port: testPort, 127 | }, 128 | want: &entity.Proxy{ 129 | Category: testHTTPSCategory, 130 | Proxy: testProxy, 131 | IP: testIP, 132 | Port: testPort, 133 | TimeTaken: 123.45, 134 | CheckedAt: time.Now().Format(time.RFC3339), 135 | }, 136 | wantError: nil, 137 | }, 138 | { 139 | name: "TestErrorParseURL", 140 | fields: fields{ 141 | fetcherUtil: &mockFetcherUtil{}, 142 | urlParserUtil: &mockURLParserUtil{ 143 | ParseFunc: func(urlStr string) (*url.URL, error) { 144 | return nil, errors.New("parse error") 145 | }, 146 | }, 147 | }, 148 | args: args{ 149 | category: testHTTPCategory, 150 | ip: testIP, 151 | port: testPort, 152 | }, 153 | want: nil, 154 | wantError: errors.New("error parsing proxy URL: parse error"), 155 | }, 156 | { 157 | name: "TestCreatingRequest", 158 | fields: fields{ 159 | fetcherUtil: &mockFetcherUtil{ 160 | NewRequestFunc: func(method, url string, body io.Reader) (*http.Request, error) { 161 | return nil, errors.New("error creating request") 162 | }, 163 | }, 164 | }, 165 | args: args{ 166 | category: testSOCKS4Category, 167 | ip: testIP, 168 | port: testPort, 169 | }, 170 | want: nil, 171 | wantError: errors.New("error creating request: error creating request"), 172 | }, 173 | { 174 | name: "TestUnsupportedProxyCategory", 175 | fields: fields{}, 176 | args: args{ 177 | category: "FTP", 178 | ip: testIP, 179 | port: testPort, 180 | }, 181 | want: nil, 182 | wantError: errors.New("proxy category FTP not supported"), 183 | }, 184 | { 185 | name: "TestRequestError", 186 | fields: fields{ 187 | fetcherUtil: &mockFetcherUtil{ 188 | DoFunc: func(client *http.Client, req *http.Request) (*http.Response, error) { 189 | return nil, fmt.Errorf("network error") 190 | }, 191 | }, 192 | urlParserUtil: &mockURLParserUtil{}, 193 | }, 194 | args: args{ 195 | category: testHTTPCategory, 196 | ip: testIP, 197 | port: testPort, 198 | }, 199 | want: nil, 200 | wantError: errors.New("request error: network error"), 201 | }, 202 | { 203 | name: "TestUnexpectedStatusCode", 204 | fields: fields{ 205 | fetcherUtil: &mockFetcherUtil{ 206 | DoFunc: func(client *http.Client, req *http.Request) (*http.Response, error) { 207 | return &http.Response{ 208 | StatusCode: http.StatusInternalServerError, 209 | Body: http.NoBody, 210 | }, nil 211 | }, 212 | }, 213 | urlParserUtil: &mockURLParserUtil{}, 214 | }, 215 | args: args{ 216 | category: testHTTPCategory, 217 | ip: testIP, 218 | port: testPort, 219 | }, 220 | want: nil, 221 | wantError: errors.New("unexpected status code 500: Internal Server Error"), 222 | }, 223 | } 224 | 225 | for _, tt := range tests { 226 | t.Run(tt.name, func(t *testing.T) { 227 | s := &ProxyService{ 228 | FetcherUtil: tt.fields.fetcherUtil, 229 | URLParserUtil: tt.fields.urlParserUtil, 230 | HTTPTestingSites: testHTTPTestingSites, 231 | HTTPSTestingSites: testHTTPSTestingSites, 232 | UserAgents: testUserAgents, 233 | Semaphore: make(chan struct{}, 10), 234 | } 235 | got, err := s.Check(tt.args.category, tt.args.ip, tt.args.port) 236 | 237 | if (err != nil && tt.wantError != nil && err.Error() != tt.wantError.Error()) || 238 | (err == nil && tt.wantError != nil) || 239 | (err != nil && tt.wantError == nil) { 240 | t.Errorf(expectedErrorButGotMessage, "ProxyService.Check()", tt.wantError, err) 241 | } 242 | 243 | if tt.want != nil && 244 | (!reflect.DeepEqual(got.Category, tt.want.Category) || 245 | !reflect.DeepEqual(got.Proxy, tt.want.Proxy) || 246 | !reflect.DeepEqual(got.IP, tt.want.IP) || 247 | !reflect.DeepEqual(got.Port, tt.want.Port)) { 248 | t.Errorf(expectedButGotMessage, "ProxyService.Check()", tt.want, got) 249 | } 250 | }) 251 | } 252 | } 253 | 254 | func TestGetTestingSite(t *testing.T) { 255 | type fields struct { 256 | httpTestingSites []string 257 | httpsTestingSites []string 258 | } 259 | 260 | tests := []struct { 261 | name string 262 | fields fields 263 | want []string 264 | }{ 265 | { 266 | name: "HTTP", 267 | fields: fields{ 268 | httpTestingSites: testHTTPTestingSites, 269 | }, 270 | want: testHTTPTestingSites, 271 | }, 272 | { 273 | name: "HTTPS", 274 | fields: fields{ 275 | httpsTestingSites: testHTTPSTestingSites, 276 | }, 277 | want: testHTTPSTestingSites, 278 | }, 279 | } 280 | 281 | for _, tt := range tests { 282 | t.Run(tt.name, func(t *testing.T) { 283 | s := &ProxyService{ 284 | HTTPTestingSites: tt.fields.httpTestingSites, 285 | HTTPSTestingSites: tt.fields.httpsTestingSites, 286 | } 287 | 288 | site := s.GetTestingSite(tt.name) 289 | if len(site) == 0 { 290 | t.Errorf(expectedNonEmptyMessage, "site", tt.name+" sites") 291 | } 292 | 293 | found := false 294 | for _, expectedSite := range tt.want { 295 | if expectedSite == site { 296 | found = true 297 | break 298 | } 299 | } 300 | if !found { 301 | t.Errorf(expectedButGotMessage, "site", tt.want, site) 302 | } 303 | }) 304 | } 305 | } 306 | 307 | func TestGetRandomUserAgent(t *testing.T) { 308 | tests := []struct { 309 | name string 310 | }{ 311 | { 312 | name: "RandomUserAgent", 313 | }, 314 | } 315 | 316 | for _, tt := range tests { 317 | t.Run(tt.name, func(t *testing.T) { 318 | s := &ProxyService{ 319 | UserAgents: testUserAgents, 320 | } 321 | site := s.GetRandomUserAgent() 322 | found := false 323 | for _, ua := range s.UserAgents { 324 | if ua == site { 325 | found = true 326 | break 327 | } 328 | } 329 | if !found { 330 | t.Errorf(expectedButGotMessage, "user agent", s.UserAgents, site) 331 | } 332 | }) 333 | } 334 | } 335 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [donate::shield]: https://img.shields.io/badge/Donate-PayPal-0070BA?logo=paypal 2 | [donate::url]: https://paypal.me/membasuh 3 | [contributors::shield]: https://img.shields.io/github/contributors/fyvri/fresh-proxy-list?style=flat 4 | [contributors::url]: https://github.com/fyvri/fresh-proxy-list/graphs/contributors 5 | [license::shield]: https://img.shields.io/badge/License-MIT-4b9081?style=flat 6 | [license::url]: https://github.com/fyvri/fresh-proxy-list/blob/HEAD/LICENSE.md 7 | [watchers::shield]: https://img.shields.io/github/watchers/fyvri/fresh-proxy-list?style=flat&logo=github&label=Watchers 8 | [watchers::url]: https://github.com/fyvri/fresh-proxy-list/watchers 9 | [stars::shield]: https://img.shields.io/github/stars/fyvri/fresh-proxy-list?style=flat&logo=github&label=Stars 10 | [stars::url]: https://github.com/fyvri/fresh-proxy-list/stargazers 11 | [forks::shield]: https://img.shields.io/github/forks/fyvri/fresh-proxy-list?style=flat&logo=github&label=Forks 12 | [forks::url]: https://github.com/fyvri/fresh-proxy-list/network/members 13 | [continuous-integration::shield]: https://github.com/fyvri/fresh-proxy-list/actions/workflows/continuous-integration.yml/badge.svg 14 | [continuous-integration::url]: https://github.com/fyvri/fresh-proxy-list/actions/workflows/continuous-integration.yml 15 | [static-analysis::shield]: https://github.com/fyvri/fresh-proxy-list/actions/workflows/static-analysis.yml/badge.svg 16 | [static-analysis::url]: https://github.com/fyvri/fresh-proxy-list/actions/workflows/static-analysis.yml 17 | [last-commit::shield]: https://img.shields.io/github/last-commit/fyvri/fresh-proxy-list?style=flat&logo=github&label=last+update 18 | [last-commit::url]: https://github.com/fyvri/fresh-proxy-list/activity?ref=archive&activity_type=branch_creation 19 | [commit-activity::shield]: https://img.shields.io/github/commit-activity/w/fyvri/fresh-proxy-list?style=flat&logo=github 20 | [commit-activity::url]: https://github.com/fyvri/fresh-proxy-list/commits/main 21 | [discussions::shield]: https://img.shields.io/github/discussions/fyvri/fresh-proxy-list?style=flat&logo=github 22 | [discussions::url]: https://github.com/fyvri/fresh-proxy-list/discussions 23 | [issues::shield]: https://img.shields.io/github/issues/fyvri/fresh-proxy-list?style=flat&logo=github 24 | [issues::url]: https://github.com/fyvri/fresh-proxy-list/issues 25 | 26 |
27 | 28 |

Fresh Proxy List

29 | 30 | [![Donate][donate::shield]][donate::url] 31 | [![License][license::shield]][license::url] 32 | [![Static Analysis][static-analysis::shield]][static-analysis::url] 33 | [![Continuous Integration][continuous-integration::shield]][continuous-integration::url] 34 |
35 | [![Last Commit][last-commit::shield]][last-commit::url] 36 | [![Commit Activity][commit-activity::shield]][commit-activity::url] 37 | [![Discussions][discussions::shield]][discussions::url] 38 | [![Issues][issues::shield]][issues::url] 39 | 40 | An automatically ⏰ updated list of free `HTTP`, `HTTPS`, `SOCKS4`, and `SOCKS5` proxies, available in multiple formats including `TXT`, `CSV`, `JSON`, `XML`, and `YAML`. The list is refreshed ⚡ **hourly** to provide the most accurate 🎯 and up-to-date information. The current data snapshot was 🚀 last updated on `Friday, December 19, 2025 at 00:25:01 (GMT+07:00)`, ensuring that users have access to the latest and most reliable proxies 🍃 available. 41 | 42 | 43 | HTTP 44 | 45 |   46 | 47 | HTTPS 48 | 49 |   50 | 51 | SOCKS4 52 | 53 |   54 | 55 | SOCKS5 56 | 57 | 58 |
59 | 60 | ## 📃 About 61 | 62 | This repository contains a free list of `HTTP/S` and `SOCKS4/5` proxies. 63 | 64 | - [x] 24/7 hourly updates (Committing since December 2024) 65 | - [x] Supported list formats: `TXT`, `CSV`, `JSON`, `XML`, and `YAML` 66 | - [x] No authentication is required when connecting any of these proxies 67 | 68 | > [!TIP] 69 | > Be sure to read this documentation. 70 | 71 |

[ back to top ]

72 | 73 | ## 🔗 Proxy List Links 74 | 75 | Duplicated proxies are removed — the only exception is if an IP has a different port open. 76 | 77 | 1. Classic View (IP:Port only) 78 | 79 | | Category | Links by File Type | 80 | | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 81 | | All | [`TXT`](https://raw.githubusercontent.com/fyvri/fresh-proxy-list/archive/storage/classic/all.txt), [`CSV`](https://raw.githubusercontent.com/fyvri/fresh-proxy-list/archive/storage/classic/all.csv), [`JSON`](https://raw.githubusercontent.com/fyvri/fresh-proxy-list/archive/storage/classic/all.json), [`XML`](https://raw.githubusercontent.com/fyvri/fresh-proxy-list/archive/storage/classic/all.xml), [`YAML`](https://raw.githubusercontent.com/fyvri/fresh-proxy-list/archive/storage/classic/all.yaml) | 82 | | HTTP | [`TXT`](https://raw.githubusercontent.com/fyvri/fresh-proxy-list/archive/storage/classic/http.txt), [`CSV`](https://raw.githubusercontent.com/fyvri/fresh-proxy-list/archive/storage/classic/http.csv), [`JSON`](https://raw.githubusercontent.com/fyvri/fresh-proxy-list/archive/storage/classic/http.json), [`XML`](https://raw.githubusercontent.com/fyvri/fresh-proxy-list/archive/storage/classic/http.xml), [`YAML`](https://raw.githubusercontent.com/fyvri/fresh-proxy-list/archive/storage/classic/http.yaml) | 83 | | HTTPS | [`TXT`](https://raw.githubusercontent.com/fyvri/fresh-proxy-list/archive/storage/classic/https.txt), [`CSV`](https://raw.githubusercontent.com/fyvri/fresh-proxy-list/archive/storage/classic/https.csv), [`JSON`](https://raw.githubusercontent.com/fyvri/fresh-proxy-list/archive/storage/classic/https.json), [`XML`](https://raw.githubusercontent.com/fyvri/fresh-proxy-list/archive/storage/classic/https.xml), [`YAML`](https://raw.githubusercontent.com/fyvri/fresh-proxy-list/archive/storage/classic/https.yaml) | 84 | | SOCKS4 | [`TXT`](https://raw.githubusercontent.com/fyvri/fresh-proxy-list/archive/storage/classic/socks4.txt), [`CSV`](https://raw.githubusercontent.com/fyvri/fresh-proxy-list/archive/storage/classic/socks4.csv), [`JSON`](https://raw.githubusercontent.com/fyvri/fresh-proxy-list/archive/storage/classic/socks4.json), [`XML`](https://raw.githubusercontent.com/fyvri/fresh-proxy-list/archive/storage/classic/socks4.xml), [`YAML`](https://raw.githubusercontent.com/fyvri/fresh-proxy-list/archive/storage/classic/socks4.yaml) | 85 | | SOCKS5 | [`TXT`](https://raw.githubusercontent.com/fyvri/fresh-proxy-list/archive/storage/classic/socks5.txt), [`CSV`](https://raw.githubusercontent.com/fyvri/fresh-proxy-list/archive/storage/classic/socks5.csv), [`JSON`](https://raw.githubusercontent.com/fyvri/fresh-proxy-list/archive/storage/classic/socks5.json), [`XML`](https://raw.githubusercontent.com/fyvri/fresh-proxy-list/archive/storage/classic/socks5.xml), [`YAML`](https://raw.githubusercontent.com/fyvri/fresh-proxy-list/archive/storage/classic/socks5.yaml) | 86 | 87 | 2. Advanced View (With full information) 88 | 89 | _Coming soon_ 90 | 91 |

[ back to top ]

92 | 93 | ## 🎁 Example Results 94 | 95 | HTTP 96 | 97 | ```txt 98 | 125.25.230.218:8080 99 | 58.141.145.86:36985 100 | 116.68.250.46:8080 101 | 128.199.20.45:8080 102 | 104.25.1.69:80 103 | 45.88.13.190:8085 104 | 45.79.44.116:8080 105 | 112.175.226.203:80 106 | 47.122.61.139:1081 107 | 103.111.22.65:58563 108 | 109 | ``` 110 | 111 | HTTPS 112 | 113 | ```txt 114 | 185.170.166.47:80 115 | 185.176.24.180:80 116 | 141.101.115.80:80 117 | 154.194.12.196:80 118 | 67.43.228.253:23381 119 | 172.67.26.132:80 120 | 103.116.7.114:80 121 | 46.254.92.233:80 122 | 188.42.88.160:80 123 | 206.238.238.108:80 124 | 125 | ``` 126 | 127 | SOCKS4 128 | 129 | ```txt 130 | 167.253.48.252:8085 131 | 8.213.129.15:8024 132 | 156.242.33.97:3129 133 | 185.221.160.24:80 134 | 209.46.30.167:80 135 | 185.104.63.107:3128 136 | 45.131.6.103:80 137 | 72.10.164.178:23229 138 | 193.233.211.67:8085 139 | 212.183.88.215:80 140 | 141 | ``` 142 | 143 | SOCKS5 144 | 145 | ```txt 146 | 27.79.213.213:16000 147 | 3.37.149.32:1080 148 | 104.27.10.176:80 149 | 132.148.128.8:55904 150 | 113.164.135.164:8080 151 | 46.161.194.83:8085 152 | 216.74.80.44:6616 153 | 147.185.161.117:80 154 | 189.137.75.168:8080 155 | 65.49.68.84:34850 156 | 157 | ``` 158 | 159 |

[ back to top ]

160 | 161 | ## 👥 Contributing 162 | 163 | If you have any ideas, [open an issue](https://github.com/fyvri/fresh-proxy-list/issues/new) and tell me what you think. 164 | 165 | Contributions are what make the open-source community such an amazing place to learn, inspire, and create. Any contributions you make are **greatly appreciated**. 166 | 167 | > [!IMPORTANT] 168 | > If you have a suggestion that would make this better, please fork the repo and create a pull request. Don't forget to give the project a star 🌟 I can't stop saying thank you! 169 | > 170 | > 1. Fork this project 171 | > 2. Create your feature branch (`git checkout -b feature/awesome-feature`) 172 | > 3. Commit your changes (`git commit -m "feat: add awesome feature"`) 173 | > 4. Push to the branch (`git push origin feature/awesome-feature`) 174 | > 5. Open a pull request 175 | 176 |

[ back to top ]

177 | 178 | ## 📜 License 179 | 180 | This project is licensed under the [MIT License](LICENSE). Feel free to use and modify it as needed. 181 | 182 |

[ back to top ]

183 | -------------------------------------------------------------------------------- /internal/usecase/proxy_usecase_test.go: -------------------------------------------------------------------------------- 1 | package usecase 2 | 3 | import ( 4 | "errors" 5 | "net" 6 | "reflect" 7 | "sync" 8 | "testing" 9 | 10 | "github.com/fyvri/fresh-proxy-list/internal/entity" 11 | "github.com/fyvri/fresh-proxy-list/internal/infrastructure/repository" 12 | "github.com/fyvri/fresh-proxy-list/internal/service" 13 | ) 14 | 15 | func TestNewProxyUsecase(t *testing.T) { 16 | mockProxyRepository := &mockProxyRepository{} 17 | mockProxyService := &mockProxyService{} 18 | proxyUsecase := NewProxyUsecase(mockProxyRepository, mockProxyService, testSpecialIPs, testPrivateIPs) 19 | if proxyUsecase == nil { 20 | t.Errorf(expectedReturnNonNil, "NewProxyUsecase", "ProxyUsecaseInterface") 21 | } 22 | 23 | uc, ok := proxyUsecase.(*ProxyUsecase) 24 | if !ok { 25 | t.Errorf(expectedTypeAssertionErrorMessage, "*ProxyUsecase") 26 | } 27 | 28 | testKey := testProxyEntity1.Category + "_" + testProxyEntity1.Proxy 29 | testValue := true 30 | uc.ProxyMap.Store(testKey, testValue) 31 | 32 | got, ok := uc.ProxyMap.Load(testKey) 33 | if !ok || got != testValue { 34 | t.Errorf(expectedButGotMessage, "value", testValue, got) 35 | } 36 | 37 | _, loaded := uc.ProxyMap.LoadOrStore(testKey, false) 38 | if !loaded { 39 | t.Errorf("Expected LoadOrStore to return true indicating the key was loaded") 40 | } 41 | 42 | got, _ = uc.ProxyMap.Load(testKey) 43 | if got != testValue { 44 | t.Errorf(expectedButGotMessage, "value after LoadOrStore", testValue, got) 45 | } 46 | 47 | if !reflect.DeepEqual(uc.SpecialIPs, testSpecialIPs) { 48 | t.Errorf(expectedButGotMessage, "SpecialIPs", testSpecialIPs, uc.SpecialIPs) 49 | } 50 | 51 | if !reflect.DeepEqual(uc.PrivateIPs, testPrivateIPs) { 52 | t.Errorf(expectedButGotMessage, "PrivateIPs", testPrivateIPs, uc.PrivateIPs) 53 | } 54 | } 55 | 56 | func TestProcessProxy(t *testing.T) { 57 | type fields struct { 58 | proxyRepository repository.ProxyRepositoryInterface 59 | proxyService service.ProxyServiceInterface 60 | } 61 | 62 | type args struct { 63 | category string 64 | proxy string 65 | isChecked bool 66 | } 67 | 68 | tests := []struct { 69 | name string 70 | fields fields 71 | args args 72 | want *entity.Proxy 73 | wantError error 74 | }{ 75 | { 76 | name: "ProxyNotFound", 77 | fields: fields{ 78 | proxyRepository: &mockProxyRepository{}, 79 | proxyService: &mockProxyService{}, 80 | }, 81 | args: args{ 82 | category: testHTTPCategory, 83 | proxy: " ", 84 | isChecked: false, 85 | }, 86 | want: nil, 87 | wantError: errors.New("proxy not found"), 88 | }, 89 | { 90 | name: "ProxyFormatIncorrect", 91 | fields: fields{ 92 | proxyRepository: &mockProxyRepository{}, 93 | proxyService: &mockProxyService{}, 94 | }, 95 | args: args{ 96 | category: testHTTPCategory, 97 | proxy: "invalid-proxy", 98 | isChecked: false, 99 | }, 100 | want: nil, 101 | wantError: errors.New("proxy format incorrect"), 102 | }, 103 | { 104 | name: "ProxyFormatNotMatch", 105 | fields: fields{ 106 | proxyRepository: &mockProxyRepository{}, 107 | proxyService: &mockProxyService{}, 108 | }, 109 | args: args{ 110 | category: testHTTPCategory, 111 | proxy: "invalid-proxy:1337", 112 | isChecked: false, 113 | }, 114 | want: nil, 115 | wantError: errors.New("proxy format not match"), 116 | }, 117 | { 118 | name: "ProxyIsSpecialIP", 119 | fields: fields{ 120 | proxyRepository: &mockProxyRepository{}, 121 | proxyService: &mockProxyService{}, 122 | }, 123 | args: args{ 124 | category: testHTTPCategory, 125 | proxy: "1.1.1.1:1337", 126 | isChecked: false, 127 | }, 128 | want: nil, 129 | wantError: errors.New("proxy belongs to special ip"), 130 | }, 131 | { 132 | name: "ProxyPortIsMoreThan65535", 133 | fields: fields{ 134 | proxyRepository: &mockProxyRepository{}, 135 | proxyService: &mockProxyService{}, 136 | }, 137 | args: args{ 138 | category: testHTTPCategory, 139 | proxy: testIP1 + ":65540", 140 | isChecked: false, 141 | }, 142 | want: nil, 143 | wantError: errors.New("proxy port format incorrect"), 144 | }, 145 | { 146 | name: "ProxyHasBeenProcessed", 147 | fields: fields{ 148 | proxyRepository: &mockProxyRepository{}, 149 | }, 150 | args: args{ 151 | category: testProxyEntity1.Category, 152 | proxy: testProxyEntity1.Proxy, 153 | isChecked: false, 154 | }, 155 | want: nil, 156 | wantError: errors.New("proxy has been processed"), 157 | }, 158 | { 159 | name: "ValidProxy", 160 | fields: fields{ 161 | proxyRepository: &mockProxyRepository{}, 162 | proxyService: &mockProxyService{ 163 | CheckFunc: func(category string, ip string, port string) (*entity.Proxy, error) { 164 | return &testProxyEntity1, nil 165 | }, 166 | }, 167 | }, 168 | args: args{ 169 | category: testProxyEntity1.Category, 170 | proxy: testProxyEntity1.Proxy, 171 | isChecked: true, 172 | }, 173 | want: &entity.Proxy{ 174 | Category: testProxyEntity1.Category, 175 | Proxy: testProxyEntity1.Proxy, 176 | IP: testProxyEntity1.IP, 177 | Port: testProxyEntity1.Port, 178 | TimeTaken: testProxyEntity1.TimeTaken, 179 | CheckedAt: testProxyEntity1.CheckedAt, 180 | }, 181 | wantError: nil, 182 | }, 183 | { 184 | name: "NotValidProxy", 185 | fields: fields{ 186 | proxyRepository: &mockProxyRepository{}, 187 | proxyService: &mockProxyService{ 188 | CheckFunc: func(category string, ip string, port string) (*entity.Proxy, error) { 189 | return nil, errors.New("proxy not valid") 190 | }, 191 | }, 192 | }, 193 | args: args{ 194 | category: testProxyEntity1.Category, 195 | proxy: testProxyEntity1.Proxy, 196 | isChecked: true, 197 | }, 198 | want: nil, 199 | wantError: errors.New("proxy not valid"), 200 | }, 201 | { 202 | name: "ValidProxyWithNotChecked", 203 | fields: fields{ 204 | proxyRepository: &mockProxyRepository{}, 205 | proxyService: &mockProxyService{ 206 | CheckFunc: func(category string, ip string, port string) (*entity.Proxy, error) { 207 | return &testProxyEntity1, nil 208 | }, 209 | }, 210 | }, 211 | args: args{ 212 | category: testProxyEntity1.Category, 213 | proxy: testProxyEntity1.Proxy, 214 | isChecked: false, 215 | }, 216 | want: &entity.Proxy{ 217 | Category: testProxyEntity1.Category, 218 | Proxy: testProxyEntity1.Proxy, 219 | IP: testProxyEntity1.IP, 220 | Port: testProxyEntity1.Port, 221 | TimeTaken: 0, 222 | CheckedAt: "", 223 | }, 224 | wantError: nil, 225 | }, 226 | } 227 | 228 | for _, tt := range tests { 229 | t.Run(tt.name, func(t *testing.T) { 230 | uc := &ProxyUsecase{ 231 | ProxyRepository: tt.fields.proxyRepository, 232 | ProxyService: tt.fields.proxyService, 233 | ProxyMap: sync.Map{}, 234 | SpecialIPs: testSpecialIPs, 235 | PrivateIPs: testPrivateIPs, 236 | } 237 | 238 | if tt.name == "ProxyHasBeenProcessed" { 239 | uc.ProxyMap.Store(tt.args.category+"_"+tt.args.proxy, true) 240 | } 241 | 242 | got, err := uc.ProcessProxy(tt.args.category, tt.args.proxy, tt.args.isChecked) 243 | 244 | if err != nil && err.Error() != tt.wantError.Error() { 245 | t.Errorf(expectedErrorButGotMessage, "ProcessProxy()", tt.wantError, err) 246 | } 247 | 248 | if !reflect.DeepEqual(got, tt.want) { 249 | t.Errorf(expectedButGotMessage, "ProcessProxy()", tt.want, got) 250 | } 251 | }) 252 | } 253 | } 254 | 255 | func TestIsSpecialIP(t *testing.T) { 256 | type args struct { 257 | ip string 258 | } 259 | 260 | type fields struct { 261 | specialIPs []string 262 | privateIPs []net.IPNet 263 | } 264 | 265 | tests := []struct { 266 | name string 267 | fields fields 268 | args args 269 | want bool 270 | }{ 271 | { 272 | name: "ItIsSpecialIP", 273 | fields: fields{ 274 | specialIPs: testSpecialIPs, 275 | privateIPs: testPrivateIPs, 276 | }, 277 | args: args{ 278 | ip: "1.1.1.1", 279 | }, 280 | want: true, 281 | }, 282 | { 283 | name: "ErrorParseIP", 284 | fields: fields{ 285 | specialIPs: testSpecialIPs, 286 | privateIPs: testPrivateIPs, 287 | }, 288 | args: args{ 289 | ip: "13.37.1", 290 | }, 291 | want: true, 292 | }, 293 | { 294 | name: "ItIsUnspecified", 295 | fields: fields{ 296 | specialIPs: testSpecialIPs, 297 | privateIPs: testPrivateIPs, 298 | }, 299 | args: args{ 300 | ip: "::1", 301 | }, 302 | want: true, 303 | }, 304 | { 305 | name: "ItIsPrivateIP", 306 | fields: fields{ 307 | specialIPs: testSpecialIPs, 308 | privateIPs: testPrivateIPs, 309 | }, 310 | args: args{ 311 | ip: "5.5.5.5", 312 | }, 313 | want: true, 314 | }, 315 | { 316 | name: "ItIsNotSpecialIP", 317 | fields: fields{ 318 | specialIPs: testSpecialIPs, 319 | privateIPs: testPrivateIPs, 320 | }, 321 | args: args{ 322 | ip: "13.37.0.1", 323 | }, 324 | want: false, 325 | }, 326 | } 327 | 328 | for _, tt := range tests { 329 | t.Run(tt.name, func(t *testing.T) { 330 | uc := &ProxyUsecase{ 331 | SpecialIPs: testSpecialIPs, 332 | PrivateIPs: testPrivateIPs, 333 | } 334 | got := uc.IsSpecialIP(tt.args.ip) 335 | if got != tt.want { 336 | t.Errorf(expectedButGotMessage, "IP: "+tt.args.ip, tt.want, got) 337 | } 338 | }) 339 | } 340 | } 341 | 342 | func TestGetAllAdvancedView(t *testing.T) { 343 | type fields struct { 344 | proxyRepository repository.ProxyRepositoryInterface 345 | } 346 | 347 | tests := []struct { 348 | name string 349 | fields fields 350 | want []entity.AdvancedProxy 351 | }{ 352 | { 353 | name: "Should return all advanced view proxies", 354 | fields: fields{ 355 | proxyRepository: &mockProxyRepository{ 356 | GetAllAdvancedViewFunc: func() []entity.AdvancedProxy { 357 | return []entity.AdvancedProxy{ 358 | testAdvancedProxyEntity1, 359 | } 360 | }, 361 | }, 362 | }, 363 | want: []entity.AdvancedProxy{ 364 | testAdvancedProxyEntity1, 365 | }, 366 | }, 367 | } 368 | 369 | for _, tt := range tests { 370 | t.Run(tt.name, func(t *testing.T) { 371 | uc := &ProxyUsecase{ 372 | ProxyRepository: tt.fields.proxyRepository, 373 | } 374 | got := uc.GetAllAdvancedView() 375 | if len(got) != len(tt.want) { 376 | t.Errorf(expectedButGotMessage, "GetAllAdvancedView()", tt.want, got) 377 | } 378 | 379 | for i, v := range got { 380 | if !reflect.DeepEqual(v, tt.want[i]) { 381 | t.Errorf(expectedButGotMessage, "GetAllAdvancedView()", tt.want[i], v) 382 | } 383 | } 384 | }) 385 | } 386 | } 387 | -------------------------------------------------------------------------------- /internal/usecase/usecase_test.go: -------------------------------------------------------------------------------- 1 | package usecase 2 | 3 | import ( 4 | "io" 5 | "net" 6 | "net/http" 7 | "net/http/httptest" 8 | "sync" 9 | "time" 10 | 11 | "github.com/fyvri/fresh-proxy-list/internal/entity" 12 | ) 13 | 14 | var ( 15 | unexpectedMessage = "Unexpected %v: %v" 16 | expectedButGotMessage = "Expected %v = %v, but got = %v" 17 | expectedErrorButGotMessage = "Expected %v error = %v, but got = %v" 18 | expectedReturnNonNil = "Expected %v to return a non-nil %v" 19 | expectedTypeAssertionErrorMessage = "Expected type assertion error, but got = %v" 20 | testStorageDir = "storage" 21 | testClassicDir = "classic" 22 | testAdvancedDir = "advanced" 23 | testTXTExtension = "txt" 24 | testCSVExtension = "csv" 25 | testJSONExtension = "json" 26 | testXMLExtension = "xml" 27 | testYAMLExtension = "yaml" 28 | testFileOutputExtensions = []string{testTXTExtension, testCSVExtension} 29 | testHTTPCategory = "HTTP" 30 | testHTTPSCategory = "HTTPS" 31 | testSOCKS4Category = "SOCKS4" 32 | testSOCKS5Category = "SOCKS5" 33 | testSpecialIPs = []string{"1.1.1.1", "2.2.2.2"} 34 | testPrivateIPs = []net.IPNet{ 35 | { 36 | IP: net.IP{3, 3, 3, 3}, 37 | Mask: net.CIDRMask(8, 32), 38 | }, 39 | { 40 | IP: net.IP{4, 4, 4, 4}, 41 | Mask: net.CIDRMask(12, 32), 42 | }, 43 | { 44 | IP: net.IP{5, 5, 5, 5}, 45 | Mask: net.CIDRMask(16, 32), 46 | }, 47 | } 48 | 49 | testIP1 = "13.37.0.1" 50 | testPort1 = "1337" 51 | testProxy1 = testIP1 + ":" + testPort1 52 | testCategory1 = testHTTPCategory 53 | testProxyEntity1 = entity.Proxy{ 54 | Proxy: testProxy1, 55 | IP: testIP1, 56 | Port: testPort1, 57 | Category: testCategory1, 58 | TimeTaken: 0, 59 | CheckedAt: time.Now().Format(time.RFC3339), 60 | } 61 | testAdvancedProxyEntity1 = entity.AdvancedProxy{ 62 | Proxy: testProxyEntity1.Proxy, 63 | IP: testProxyEntity1.IP, 64 | Port: testProxyEntity1.Port, 65 | TimeTaken: testProxyEntity1.TimeTaken, 66 | CheckedAt: testProxyEntity1.CheckedAt, 67 | Categories: []string{ 68 | testCategory1, 69 | }, 70 | } 71 | 72 | testIP2 = "13.37.0.2" 73 | testPort2 = "1337" 74 | testProxy2 = testIP2 + ":" + testPort2 75 | testCategory2 = testHTTPSCategory 76 | testProxyEntity2 = entity.Proxy{ 77 | Proxy: testProxy2, 78 | IP: testIP2, 79 | Port: testPort2, 80 | Category: testCategory2, 81 | TimeTaken: 0, 82 | CheckedAt: time.Now().Format(time.RFC3339), 83 | } 84 | testAdvancedProxyEntity2 = entity.AdvancedProxy{ 85 | Proxy: testProxyEntity2.Proxy, 86 | IP: testProxyEntity2.IP, 87 | Port: testProxyEntity2.Port, 88 | TimeTaken: testProxyEntity2.TimeTaken, 89 | CheckedAt: testProxyEntity2.CheckedAt, 90 | Categories: []string{ 91 | testCategory2, 92 | }, 93 | } 94 | 95 | testIP3 = "13.37.0.3" 96 | testPort3 = "1337" 97 | testProxy3 = testIP3 + ":" + testPort3 98 | testCategory3 = testSOCKS4Category 99 | testProxyEntity3 = entity.Proxy{ 100 | Proxy: testProxy3, 101 | IP: testIP3, 102 | Port: testPort3, 103 | Category: testCategory3, 104 | TimeTaken: 0, 105 | CheckedAt: time.Now().Format(time.RFC3339), 106 | } 107 | testAdvancedProxyEntity3 = entity.AdvancedProxy{ 108 | Proxy: testProxyEntity3.Proxy, 109 | IP: testProxyEntity3.IP, 110 | Port: testProxyEntity3.Port, 111 | TimeTaken: testProxyEntity3.TimeTaken, 112 | CheckedAt: testProxyEntity3.CheckedAt, 113 | Categories: []string{ 114 | testCategory3, 115 | }, 116 | } 117 | 118 | testIP4 = "13.37.0.4" 119 | testPort4 = "1337" 120 | testProxy4 = testIP4 + ":" + testPort4 121 | testCategory4 = testSOCKS5Category 122 | testProxyEntity4 = entity.Proxy{ 123 | Proxy: testProxy4, 124 | IP: testIP4, 125 | Port: testPort4, 126 | Category: testCategory4, 127 | TimeTaken: 0, 128 | CheckedAt: time.Now().Format(time.RFC3339), 129 | } 130 | testAdvancedProxyEntity4 = entity.AdvancedProxy{ 131 | Proxy: testProxyEntity4.Proxy, 132 | IP: testProxyEntity4.IP, 133 | Port: testProxyEntity4.Port, 134 | TimeTaken: testProxyEntity4.TimeTaken, 135 | CheckedAt: testProxyEntity4.CheckedAt, 136 | Categories: []string{ 137 | testCategory4, 138 | }, 139 | } 140 | ) 141 | 142 | type mockFetcherUtil struct { 143 | fetchDataByte []byte 144 | fetcherError error 145 | NewRequestFunc func(method, url string, body io.Reader) (*http.Request, error) 146 | DoFunc func(client *http.Client, req *http.Request) (*http.Response, error) 147 | } 148 | 149 | func (m *mockFetcherUtil) FetchData(url string) ([]byte, error) { 150 | if m.fetcherError != nil { 151 | return nil, m.fetcherError 152 | } 153 | return m.fetchDataByte, nil 154 | } 155 | 156 | func (m *mockFetcherUtil) Do(client *http.Client, req *http.Request) (*http.Response, error) { 157 | if m.DoFunc != nil { 158 | return m.DoFunc(client, req) 159 | } 160 | return httptest.NewRecorder().Result(), nil 161 | } 162 | 163 | func (m *mockFetcherUtil) NewRequest(method string, url string, body io.Reader) (*http.Request, error) { 164 | if m.NewRequestFunc != nil { 165 | return m.NewRequestFunc(method, url, body) 166 | } 167 | return http.NewRequest(method, url, body) 168 | } 169 | 170 | type mockProxyService struct { 171 | CheckFunc func(category string, ip string, port string) (*entity.Proxy, error) 172 | GetTestingSiteFunc func(category string) string 173 | GetRandomUserAgentFunc func() string 174 | } 175 | 176 | func (m *mockProxyService) Check(category string, ip string, port string) (*entity.Proxy, error) { 177 | if m.CheckFunc != nil { 178 | return m.CheckFunc(category, ip, port) 179 | } 180 | return nil, nil 181 | } 182 | 183 | func (m *mockProxyService) GetTestingSite(category string) string { 184 | if m.GetTestingSiteFunc != nil { 185 | return m.GetTestingSiteFunc(category) 186 | } 187 | return "" 188 | } 189 | 190 | func (m *mockProxyService) GetRandomUserAgent() string { 191 | if m.GetRandomUserAgentFunc != nil { 192 | return m.GetRandomUserAgentFunc() 193 | } 194 | return "" 195 | } 196 | 197 | type mockSourceRepository struct { 198 | LoadSourcesFunc func() ([]entity.Source, error) 199 | } 200 | 201 | func (m *mockSourceRepository) LoadSources() ([]entity.Source, error) { 202 | return m.LoadSourcesFunc() 203 | } 204 | 205 | type mockFileRepository struct { 206 | SaveFileFunc func(filename string, data interface{}, format string) error 207 | CreateDirectoryFunc func(filePath string) error 208 | WriteTxtFunc func(writer io.Writer, data interface{}) error 209 | EncodeCSVFunc func(writer io.Writer, data interface{}) error 210 | WriteCSVFunc func(writer io.Writer, header []string, rows [][]string) error 211 | EncodeJSONFunc func(writer io.Writer, data interface{}) error 212 | EncodeXMLFunc func(writer io.Writer, data interface{}) error 213 | EncodeYAMLFunc func(writer io.Writer, data interface{}) error 214 | } 215 | 216 | func (m *mockFileRepository) SaveFile(filename string, data interface{}, ext string) error { 217 | if m.SaveFileFunc != nil { 218 | return m.SaveFileFunc(filename, data, ext) 219 | } 220 | return nil 221 | } 222 | 223 | func (m *mockFileRepository) CreateDirectory(filePath string) error { 224 | if m.CreateDirectoryFunc != nil { 225 | return m.CreateDirectoryFunc(filePath) 226 | } 227 | return nil 228 | } 229 | 230 | func (m *mockFileRepository) WriteTxt(writer io.Writer, data interface{}) error { 231 | if m.WriteTxtFunc != nil { 232 | return m.WriteTxtFunc(writer, data) 233 | } 234 | return nil 235 | } 236 | 237 | func (m *mockFileRepository) EncodeCSV(writer io.Writer, data interface{}) error { 238 | if m.EncodeCSVFunc != nil { 239 | return m.EncodeCSVFunc(writer, data) 240 | } 241 | return nil 242 | } 243 | 244 | func (m *mockFileRepository) WriteCSV(writer io.Writer, header []string, rows [][]string) error { 245 | if m.WriteCSVFunc != nil { 246 | return m.WriteCSVFunc(writer, header, rows) 247 | } 248 | return nil 249 | } 250 | 251 | func (m *mockFileRepository) EncodeJSON(writer io.Writer, data interface{}) error { 252 | if m.EncodeJSONFunc != nil { 253 | return m.EncodeJSONFunc(writer, data) 254 | } 255 | return nil 256 | } 257 | 258 | func (m *mockFileRepository) EncodeXML(writer io.Writer, data interface{}) error { 259 | if m.EncodeXMLFunc != nil { 260 | return m.EncodeXMLFunc(writer, data) 261 | } 262 | return nil 263 | } 264 | 265 | func (m *mockFileRepository) EncodeYAML(writer io.Writer, data interface{}) error { 266 | if m.EncodeYAMLFunc != nil { 267 | return m.EncodeYAMLFunc(writer, data) 268 | } 269 | return nil 270 | } 271 | 272 | type mockProxyRepository struct { 273 | StoreFunc func(proxy *entity.Proxy) 274 | GetAllClassicViewFunc func() []string 275 | GetHTTPClassicViewFunc func() []string 276 | GetHTTPSClassicViewFunc func() []string 277 | GetSOCKS4ClassicViewFunc func() []string 278 | GetSOCKS5ClassicViewFunc func() []string 279 | GetAllAdvancedViewFunc func() []entity.AdvancedProxy 280 | GetHTTPAdvancedViewFunc func() []entity.Proxy 281 | GetHTTPSAdvancedViewFunc func() []entity.Proxy 282 | GetSOCKS4AdvancedViewFunc func() []entity.Proxy 283 | GetSOCKS5AdvancedViewFunc func() []entity.Proxy 284 | 285 | StoredProxies []entity.Proxy 286 | Mutex sync.Mutex 287 | } 288 | 289 | func (m *mockProxyRepository) Store(proxy *entity.Proxy) { 290 | m.Mutex.Lock() 291 | defer m.Mutex.Unlock() 292 | m.StoredProxies = append(m.StoredProxies, *proxy) 293 | } 294 | 295 | func (m *mockProxyRepository) GetStoredProxies() []entity.Proxy { 296 | return m.StoredProxies 297 | } 298 | 299 | func (m *mockProxyRepository) GetAllClassicView() []string { 300 | if m.GetAllClassicViewFunc != nil { 301 | return m.GetAllClassicViewFunc() 302 | } 303 | return nil 304 | } 305 | 306 | func (m *mockProxyRepository) GetHTTPClassicView() []string { 307 | if m.GetHTTPClassicViewFunc != nil { 308 | return m.GetHTTPClassicViewFunc() 309 | } 310 | return nil 311 | } 312 | 313 | func (m *mockProxyRepository) GetHTTPSClassicView() []string { 314 | if m.GetHTTPSClassicViewFunc != nil { 315 | return m.GetHTTPSClassicViewFunc() 316 | } 317 | return nil 318 | } 319 | 320 | func (m *mockProxyRepository) GetSOCKS4ClassicView() []string { 321 | if m.GetSOCKS4ClassicViewFunc != nil { 322 | return m.GetSOCKS4ClassicViewFunc() 323 | } 324 | return nil 325 | } 326 | 327 | func (m *mockProxyRepository) GetSOCKS5ClassicView() []string { 328 | if m.GetSOCKS5ClassicViewFunc != nil { 329 | return m.GetSOCKS5ClassicViewFunc() 330 | } 331 | return nil 332 | } 333 | 334 | func (m *mockProxyRepository) GetAllAdvancedView() []entity.AdvancedProxy { 335 | if m.GetAllAdvancedViewFunc != nil { 336 | return m.GetAllAdvancedViewFunc() 337 | } 338 | return nil 339 | } 340 | 341 | func (m *mockProxyRepository) GetHTTPAdvancedView() []entity.Proxy { 342 | if m.GetHTTPAdvancedViewFunc != nil { 343 | return m.GetHTTPAdvancedViewFunc() 344 | } 345 | return nil 346 | } 347 | 348 | func (m *mockProxyRepository) GetHTTPSAdvancedView() []entity.Proxy { 349 | if m.GetHTTPSAdvancedViewFunc != nil { 350 | return m.GetHTTPSAdvancedViewFunc() 351 | } 352 | return nil 353 | } 354 | 355 | func (m *mockProxyRepository) GetSOCKS4AdvancedView() []entity.Proxy { 356 | if m.GetSOCKS4AdvancedViewFunc != nil { 357 | return m.GetSOCKS4AdvancedViewFunc() 358 | } 359 | return nil 360 | } 361 | 362 | func (m *mockProxyRepository) GetSOCKS5AdvancedView() []entity.Proxy { 363 | if m.GetSOCKS5AdvancedViewFunc != nil { 364 | return m.GetSOCKS5AdvancedViewFunc() 365 | } 366 | return nil 367 | } 368 | -------------------------------------------------------------------------------- /internal/infrastructure/repository/file_repository_test.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "fmt" 7 | "io" 8 | "io/fs" 9 | "os" 10 | "strings" 11 | "testing" 12 | 13 | "github.com/fyvri/fresh-proxy-list/pkg/utils" 14 | ) 15 | 16 | var ( 17 | column1 = "Column1" 18 | column2 = "Column2" 19 | row1Column1 = "Row1Column1" 20 | row1Column2 = "Row1Column2" 21 | row2Column1 = "Row2Column1" 22 | row2Column2 = "Row2Column2" 23 | ) 24 | 25 | func TestNewFileRepository(t *testing.T) { 26 | mockMkdirAll := func(path string, perm fs.FileMode) error { 27 | if path == "" { 28 | return errors.New("path cannot be empty") 29 | } 30 | return nil 31 | } 32 | mockCreate := func(name string) (io.Writer, error) { 33 | if name == "" { 34 | return nil, errors.New("file name cannot be empty") 35 | } 36 | return &bytes.Buffer{}, nil 37 | } 38 | mockCSVWriterUtil := &mockCSVWriterUtil{} 39 | fileRepository := NewFileRepository(mockMkdirAll, mockCreate, mockCSVWriterUtil) 40 | 41 | if fileRepository == nil { 42 | t.Errorf(expectedReturnNonNil, "NewFileRepository", "FileRepositoryInterface") 43 | } 44 | 45 | r, ok := fileRepository.(*FileRepository) 46 | if !ok { 47 | t.Errorf(expectedTypeAssertionErrorMessage, "*FileRepository") 48 | } 49 | 50 | if r.MkdirAll == nil { 51 | t.Errorf("expected mkdirAll to be set") 52 | } 53 | 54 | if r.Create == nil { 55 | t.Errorf("expected create to be set") 56 | } 57 | } 58 | 59 | func TestSaveFile(t *testing.T) { 60 | type fields struct { 61 | mkdirAll func(path string, perm os.FileMode) error 62 | create func(name string) (io.Writer, error) 63 | csvWriter utils.CSVWriterUtilInterface 64 | } 65 | 66 | type args struct { 67 | path string 68 | data interface{} 69 | format string 70 | } 71 | 72 | tests := []struct { 73 | name string 74 | fields fields 75 | args args 76 | want string 77 | wantError error 78 | }{ 79 | { 80 | name: "CreateDirectoryError", 81 | fields: fields{ 82 | mkdirAll: func(path string, perm os.FileMode) error { 83 | return errors.New("error creating directory") 84 | }, 85 | create: func(name string) (io.Writer, error) { 86 | return nil, nil 87 | }, 88 | }, 89 | args: args{ 90 | path: testClassicFilePath + "." + testTXTExtension, 91 | data: strings.Join(testIPs, "\n"), 92 | format: testTXTExtension, 93 | }, 94 | want: "", 95 | wantError: fmt.Errorf("error creating directory %v: %v", testClassicFilePath+"."+testTXTExtension, "error creating directory"), 96 | }, 97 | { 98 | name: "CreateFileError", 99 | fields: fields{ 100 | mkdirAll: func(path string, perm os.FileMode) error { 101 | return nil 102 | }, 103 | create: func(name string) (io.Writer, error) { 104 | return nil, errors.New("error creating file") 105 | }, 106 | }, 107 | args: args{ 108 | path: testClassicFilePath + "." + testTXTExtension, 109 | data: testIPs, 110 | format: testTXTExtension, 111 | }, 112 | want: "", 113 | wantError: fmt.Errorf("error creating file %v: %v", testClassicFilePath+"."+testTXTExtension, "error creating file"), 114 | }, 115 | { 116 | name: "UnsupportedFormat", 117 | fields: fields{ 118 | mkdirAll: func(path string, perm os.FileMode) error { 119 | return nil 120 | }, 121 | create: func(name string) (io.Writer, error) { 122 | return &bytes.Buffer{}, nil 123 | }, 124 | }, 125 | args: args{ 126 | path: testClassicFilePath + "." + testTXTExtension, 127 | data: testIPs, 128 | format: "unsupported-format", 129 | }, 130 | want: "", 131 | wantError: fmt.Errorf("unsupported format: %v", "unsupported-format"), 132 | }, 133 | { 134 | name: "WriteTXTSuccess", 135 | fields: fields{ 136 | mkdirAll: func(path string, perm os.FileMode) error { 137 | return nil 138 | }, 139 | create: func(name string) (io.Writer, error) { 140 | return &bytes.Buffer{}, nil 141 | }, 142 | }, 143 | args: args{ 144 | path: testClassicFilePath + "." + testTXTExtension, 145 | data: testIPs, 146 | format: testTXTExtension, 147 | }, 148 | want: testIP1 + testIP2, 149 | wantError: nil, 150 | }, 151 | { 152 | name: "WriteTXTError", 153 | fields: fields{ 154 | mkdirAll: func(path string, perm os.FileMode) error { 155 | return nil 156 | }, 157 | create: func(name string) (io.Writer, error) { 158 | return &mockWriter{ 159 | errWrite: errors.New(testErrorWriting), 160 | }, nil 161 | }, 162 | }, 163 | args: args{ 164 | path: testClassicFilePath + "." + testTXTExtension, 165 | data: testIPs, 166 | format: testTXTExtension, 167 | }, 168 | want: "", 169 | wantError: fmt.Errorf("error writing TXT: %v", testErrorWriting), 170 | }, 171 | { 172 | name: "EncodeJSON", 173 | fields: fields{ 174 | mkdirAll: func(path string, perm os.FileMode) error { 175 | return nil 176 | }, 177 | create: func(name string) (io.Writer, error) { 178 | return &bytes.Buffer{}, nil 179 | }, 180 | }, 181 | args: args{ 182 | path: testClassicFilePath + "." + testJSONExtension, 183 | data: string(testProxiesToString), 184 | format: testJSONExtension, 185 | }, 186 | want: string(testProxiesToString), 187 | wantError: nil, 188 | }, 189 | { 190 | name: "EncodeJSONError", 191 | fields: fields{ 192 | mkdirAll: func(path string, perm os.FileMode) error { 193 | return nil 194 | }, 195 | create: func(name string) (io.Writer, error) { 196 | return &mockWriter{ 197 | errWrite: errors.New(testErrorWriting), 198 | }, nil 199 | }, 200 | }, 201 | args: args{ 202 | path: testClassicFilePath + "." + testJSONExtension, 203 | data: testProxies, 204 | format: testJSONExtension, 205 | }, 206 | want: "", 207 | wantError: fmt.Errorf(testErrorEncode, "JSON", testErrorWriting), 208 | }, 209 | { 210 | name: "EncodeCSVWithStringData", 211 | fields: fields{ 212 | mkdirAll: func(path string, perm os.FileMode) error { 213 | return nil 214 | }, 215 | create: func(name string) (io.Writer, error) { 216 | return &bytes.Buffer{}, nil 217 | }, 218 | csvWriter: &mockCSVWriterUtil{}, 219 | }, 220 | args: args{ 221 | path: testClassicFilePath + "." + testCSVExtension, 222 | data: testIPs, 223 | format: testCSVExtension, 224 | }, 225 | want: string(testIPsToString) + "\n", 226 | wantError: nil, 227 | }, 228 | { 229 | name: "EncodeCSVWithProxyData", 230 | fields: fields{ 231 | mkdirAll: func(path string, perm os.FileMode) error { 232 | return nil 233 | }, 234 | create: func(name string) (io.Writer, error) { 235 | return &bytes.Buffer{}, nil 236 | }, 237 | csvWriter: &mockCSVWriterUtil{}, 238 | }, 239 | args: args{ 240 | path: testAdvancedFilePath + "." + testCSVExtension, 241 | data: testProxies, 242 | format: testCSVExtension, 243 | }, 244 | want: string(testProxiesToString) + "\n", 245 | wantError: nil, 246 | }, 247 | { 248 | name: "EncodeCSVWithAdvancedProxyData", 249 | fields: fields{ 250 | mkdirAll: func(path string, perm os.FileMode) error { 251 | return nil 252 | }, 253 | create: func(name string) (io.Writer, error) { 254 | return &bytes.Buffer{}, nil 255 | }, 256 | csvWriter: &mockCSVWriterUtil{}, 257 | }, 258 | args: args{ 259 | path: testAdvancedFilePath + "." + testCSVExtension, 260 | data: testAdvancedProxies, 261 | format: testCSVExtension, 262 | }, 263 | want: string(testAdvancedProxiesToString) + "\n", 264 | wantError: nil, 265 | }, 266 | { 267 | name: "EncodeCSVWithErrorDataType", 268 | fields: fields{ 269 | mkdirAll: func(path string, perm os.FileMode) error { 270 | return nil 271 | }, 272 | create: func(name string) (io.Writer, error) { 273 | return &bytes.Buffer{}, nil 274 | }, 275 | csvWriter: &mockCSVWriterUtil{}, 276 | }, 277 | args: args{ 278 | path: testClassicFilePath + "." + testCSVExtension, 279 | data: []error{}, 280 | format: testCSVExtension, 281 | }, 282 | want: "", 283 | wantError: errors.New("invalid data type for CSV encoding"), 284 | }, 285 | { 286 | name: "EncodeXMLWithStringStruct", 287 | fields: fields{ 288 | mkdirAll: func(path string, perm os.FileMode) error { 289 | return nil 290 | }, 291 | create: func(name string) (io.Writer, error) { 292 | return &bytes.Buffer{}, nil 293 | }, 294 | }, 295 | args: args{ 296 | path: testClassicFilePath + "." + testXMLExtension, 297 | data: testIPs, 298 | format: testXMLExtension, 299 | }, 300 | want: string(testIPsToString), 301 | wantError: nil, 302 | }, 303 | { 304 | name: "EncodeXMLWithProxyStruct", 305 | fields: fields{ 306 | mkdirAll: func(path string, perm os.FileMode) error { 307 | return nil 308 | }, 309 | create: func(name string) (io.Writer, error) { 310 | return &bytes.Buffer{}, nil 311 | }, 312 | }, 313 | args: args{ 314 | path: testClassicFilePath + "." + testXMLExtension, 315 | data: testProxies, 316 | format: testXMLExtension, 317 | }, 318 | want: string(testProxiesToString), 319 | wantError: nil, 320 | }, 321 | { 322 | name: "EncodeXMLWithAdvancedProxyStruct", 323 | fields: fields{ 324 | mkdirAll: func(path string, perm os.FileMode) error { 325 | return nil 326 | }, 327 | create: func(name string) (io.Writer, error) { 328 | return &bytes.Buffer{}, nil 329 | }, 330 | }, 331 | args: args{ 332 | path: testClassicFilePath + "." + testXMLExtension, 333 | data: testAdvancedProxies, 334 | format: testXMLExtension, 335 | }, 336 | want: string(testAdvancedProxiesToString), 337 | wantError: nil, 338 | }, 339 | { 340 | name: "EncodeXMLError", 341 | fields: fields{ 342 | mkdirAll: func(path string, perm os.FileMode) error { 343 | return nil 344 | }, 345 | create: func(name string) (io.Writer, error) { 346 | return &mockWriter{ 347 | errWrite: errors.New(testErrorWriting), 348 | }, nil 349 | }, 350 | }, 351 | args: args{ 352 | path: testClassicFilePath + "." + testXMLExtension, 353 | data: testProxies, 354 | format: testXMLExtension, 355 | }, 356 | want: "", 357 | wantError: fmt.Errorf(testErrorEncode, "XML", testErrorWriting), 358 | }, 359 | { 360 | name: "EncodeYAMLWithStringStruct", 361 | fields: fields{ 362 | mkdirAll: func(path string, perm os.FileMode) error { 363 | return nil 364 | }, 365 | create: func(name string) (io.Writer, error) { 366 | return &bytes.Buffer{}, nil 367 | }, 368 | }, 369 | args: args{ 370 | path: testClassicFilePath + "." + testYAMLExtension, 371 | data: testIPs, 372 | format: testYAMLExtension, 373 | }, 374 | want: string(testIPsToString), 375 | wantError: nil, 376 | }, 377 | { 378 | name: "EncodeYAMLWithProxyStruct", 379 | fields: fields{ 380 | mkdirAll: func(path string, perm os.FileMode) error { 381 | return nil 382 | }, 383 | create: func(name string) (io.Writer, error) { 384 | return &bytes.Buffer{}, nil 385 | }, 386 | }, 387 | args: args{ 388 | path: testClassicFilePath + "." + testYAMLExtension, 389 | data: testProxies, 390 | format: testYAMLExtension, 391 | }, 392 | want: string(testProxiesToString), 393 | wantError: nil, 394 | }, 395 | { 396 | name: "EncodeYAMLWithAdvancedProxyStruct", 397 | fields: fields{ 398 | mkdirAll: func(path string, perm os.FileMode) error { 399 | return nil 400 | }, 401 | create: func(name string) (io.Writer, error) { 402 | return &bytes.Buffer{}, nil 403 | }, 404 | }, 405 | args: args{ 406 | path: testClassicFilePath + "." + testYAMLExtension, 407 | data: testAdvancedProxies, 408 | format: testYAMLExtension, 409 | }, 410 | want: string(testAdvancedProxiesToString), 411 | wantError: nil, 412 | }, 413 | { 414 | name: "EncodeYAMLError", 415 | fields: fields{ 416 | mkdirAll: func(path string, perm os.FileMode) error { 417 | return nil 418 | }, 419 | create: func(name string) (io.Writer, error) { 420 | return &mockWriter{ 421 | errWrite: errors.New(testErrorWriting), 422 | }, nil 423 | }, 424 | }, 425 | args: args{ 426 | path: testClassicFilePath + "." + testYAMLExtension, 427 | data: testProxies, 428 | format: testYAMLExtension, 429 | }, 430 | want: "", 431 | wantError: fmt.Errorf(testErrorEncode, "YAML", "yaml: write error: error writing"), 432 | }, 433 | } 434 | 435 | for _, tt := range tests { 436 | t.Run(tt.name, func(t *testing.T) { 437 | r := &FileRepository{ 438 | MkdirAll: tt.fields.mkdirAll, 439 | Create: tt.fields.create, 440 | CSVWriter: tt.fields.csvWriter, 441 | } 442 | err := r.SaveFile(tt.args.path, tt.args.data, tt.args.format) 443 | if (err != nil && tt.wantError != nil && err.Error() != tt.wantError.Error()) || 444 | (err == nil && tt.wantError != nil) || 445 | (err != nil && tt.wantError == nil) { 446 | t.Errorf(expectedErrorButGotMessage, "SaveFile()", tt.wantError, err) 447 | } 448 | }) 449 | } 450 | } 451 | 452 | func TestWriteCSV(t *testing.T) { 453 | type fields struct { 454 | csvWriter utils.CSVWriterUtilInterface 455 | } 456 | 457 | type args struct { 458 | header []string 459 | rows [][]string 460 | } 461 | 462 | tests := []struct { 463 | name string 464 | fields fields 465 | args args 466 | wantError error 467 | }{ 468 | { 469 | name: "Success", 470 | fields: fields{ 471 | csvWriter: &mockCSVWriterUtil{}, 472 | }, 473 | args: args{ 474 | header: []string{column1, column2}, 475 | rows: [][]string{ 476 | {row1Column1, row1Column2}, 477 | {row2Column1, row2Column2}, 478 | }, 479 | }, 480 | wantError: nil, 481 | }, 482 | { 483 | name: "ErrorWritingHeader", 484 | fields: fields{ 485 | csvWriter: &mockCSVWriterUtil{ 486 | errWrite: errors.New("write header error"), 487 | }, 488 | }, 489 | args: args{ 490 | header: []string{column1, column2}, 491 | rows: [][]string{ 492 | {row1Column1, row1Column2}, 493 | {row2Column1, row2Column2}, 494 | }, 495 | }, 496 | wantError: fmt.Errorf("failed to write header: %w", errors.New("write header error")), 497 | }, 498 | { 499 | name: "ErrorWritingRow", 500 | fields: fields{ 501 | csvWriter: &mockCSVWriterUtil{ 502 | errWrite: errors.New("write row error"), 503 | }, 504 | }, 505 | args: args{ 506 | header: nil, 507 | rows: [][]string{ 508 | {row1Column1, row1Column2}, 509 | {row2Column1, row2Column2}, 510 | }, 511 | }, 512 | wantError: fmt.Errorf("failed to write row: %w", errors.New("write row error")), 513 | }, 514 | } 515 | 516 | for _, tt := range tests { 517 | t.Run(tt.name, func(t *testing.T) { 518 | var buf bytes.Buffer 519 | r := &FileRepository{ 520 | CSVWriter: tt.fields.csvWriter, 521 | } 522 | err := r.WriteCSV(&buf, tt.args.header, tt.args.rows) 523 | if (err != nil && tt.wantError != nil && err.Error() != tt.wantError.Error()) || 524 | (err == nil && tt.wantError != nil) || 525 | (err != nil && tt.wantError == nil) { 526 | t.Errorf(expectedErrorButGotMessage, "WriteCSV()", tt.wantError, err) 527 | } 528 | }) 529 | } 530 | } 531 | -------------------------------------------------------------------------------- /internal/infrastructure/repository/proxy_repository_test.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | 7 | "github.com/fyvri/fresh-proxy-list/internal/entity" 8 | ) 9 | 10 | func TestNewProxyRepository(t *testing.T) { 11 | tests := []struct { 12 | name string 13 | want ProxyRepositoryInterface 14 | }{ 15 | { 16 | name: "Success", 17 | want: &ProxyRepository{}, 18 | }, 19 | } 20 | 21 | for _, tt := range tests { 22 | t.Run(tt.name, func(t *testing.T) { 23 | proxyRepository := NewProxyRepository() 24 | 25 | if proxyRepository == nil { 26 | t.Errorf(expectedReturnNonNil, "NewProxyRepository", "ProxyRepositoryInterface") 27 | } 28 | 29 | got, ok := proxyRepository.(*ProxyRepository) 30 | if !ok { 31 | t.Errorf(expectedTypeAssertionErrorMessage, "*ProxyRepository") 32 | } 33 | 34 | if !reflect.DeepEqual(tt.want, got) { 35 | t.Errorf(expectedButGotMessage, "*ProxyRepository", tt.want, got) 36 | } 37 | }) 38 | } 39 | } 40 | 41 | func TestProxyRepository(t *testing.T) { 42 | type fields struct { 43 | allClassicView []string 44 | httpClassicView []string 45 | httpsClassicView []string 46 | socks4ClassicView []string 47 | socks5ClassicView []string 48 | allAdvancedView []entity.AdvancedProxy 49 | httpAdvancedView []entity.Proxy 50 | httpsAdvancedView []entity.Proxy 51 | socks4AdvancedView []entity.Proxy 52 | socks5AdvancedView []entity.Proxy 53 | } 54 | 55 | type args struct { 56 | proxy entity.Proxy 57 | } 58 | 59 | tests := []struct { 60 | name string 61 | fields fields 62 | args args 63 | want fields 64 | wantErr error 65 | }{ 66 | { 67 | name: "StoreHTTPProxy", 68 | fields: fields{ 69 | allClassicView: []string{}, 70 | httpClassicView: []string{}, 71 | httpsClassicView: []string{}, 72 | socks4ClassicView: []string{}, 73 | socks5ClassicView: []string{}, 74 | allAdvancedView: []entity.AdvancedProxy{}, 75 | httpAdvancedView: []entity.Proxy{}, 76 | httpsAdvancedView: []entity.Proxy{}, 77 | socks4AdvancedView: []entity.Proxy{}, 78 | socks5AdvancedView: []entity.Proxy{}, 79 | }, 80 | args: args{ 81 | proxy: testProxyEntity1, 82 | }, 83 | want: fields{ 84 | allClassicView: []string{ 85 | testProxy1, 86 | }, 87 | httpClassicView: []string{ 88 | testProxy1, 89 | }, 90 | httpsClassicView: []string{}, 91 | socks4ClassicView: []string{}, 92 | socks5ClassicView: []string{}, 93 | allAdvancedView: []entity.AdvancedProxy{ 94 | testAdvancedProxyEntity1, 95 | }, 96 | httpAdvancedView: []entity.Proxy{ 97 | testProxyEntity1, 98 | }, 99 | httpsAdvancedView: []entity.Proxy{}, 100 | socks4AdvancedView: []entity.Proxy{}, 101 | socks5AdvancedView: []entity.Proxy{}, 102 | }, 103 | wantErr: nil, 104 | }, 105 | { 106 | name: "StoreHTTPSProxy", 107 | fields: fields{ 108 | allClassicView: []string{}, 109 | httpClassicView: []string{}, 110 | httpsClassicView: []string{}, 111 | socks4ClassicView: []string{}, 112 | socks5ClassicView: []string{}, 113 | allAdvancedView: []entity.AdvancedProxy{}, 114 | httpAdvancedView: []entity.Proxy{}, 115 | httpsAdvancedView: []entity.Proxy{}, 116 | socks4AdvancedView: []entity.Proxy{}, 117 | socks5AdvancedView: []entity.Proxy{}, 118 | }, 119 | args: args{ 120 | proxy: testProxyEntity2, 121 | }, 122 | want: fields{ 123 | allClassicView: []string{ 124 | testProxy2, 125 | }, 126 | httpClassicView: []string{}, 127 | httpsClassicView: []string{ 128 | testProxy2, 129 | }, 130 | socks4ClassicView: []string{}, 131 | socks5ClassicView: []string{}, 132 | allAdvancedView: []entity.AdvancedProxy{ 133 | testAdvancedProxyEntity2, 134 | }, 135 | httpAdvancedView: []entity.Proxy{}, 136 | httpsAdvancedView: []entity.Proxy{ 137 | testProxyEntity2, 138 | }, 139 | socks4AdvancedView: []entity.Proxy{}, 140 | socks5AdvancedView: []entity.Proxy{}, 141 | }, 142 | wantErr: nil, 143 | }, 144 | { 145 | name: "StoreSOCKS4Proxy", 146 | fields: fields{ 147 | allClassicView: []string{}, 148 | httpClassicView: []string{}, 149 | httpsClassicView: []string{}, 150 | socks4ClassicView: []string{}, 151 | socks5ClassicView: []string{}, 152 | allAdvancedView: []entity.AdvancedProxy{}, 153 | httpAdvancedView: []entity.Proxy{}, 154 | httpsAdvancedView: []entity.Proxy{}, 155 | socks4AdvancedView: []entity.Proxy{}, 156 | socks5AdvancedView: []entity.Proxy{}, 157 | }, 158 | args: args{ 159 | proxy: testProxyEntity3, 160 | }, 161 | want: fields{ 162 | allClassicView: []string{ 163 | testProxy3, 164 | }, 165 | httpClassicView: []string{}, 166 | httpsClassicView: []string{}, 167 | socks4ClassicView: []string{ 168 | testProxy3, 169 | }, 170 | socks5ClassicView: []string{}, 171 | allAdvancedView: []entity.AdvancedProxy{ 172 | testAdvancedProxyEntity3, 173 | }, 174 | httpAdvancedView: []entity.Proxy{}, 175 | httpsAdvancedView: []entity.Proxy{}, 176 | socks4AdvancedView: []entity.Proxy{ 177 | testProxyEntity3, 178 | }, 179 | socks5AdvancedView: []entity.Proxy{}, 180 | }, 181 | wantErr: nil, 182 | }, 183 | { 184 | name: "StoreSOCKS5Proxy", 185 | fields: fields{ 186 | allClassicView: []string{}, 187 | httpClassicView: []string{}, 188 | httpsClassicView: []string{}, 189 | socks4ClassicView: []string{}, 190 | socks5ClassicView: []string{}, 191 | allAdvancedView: []entity.AdvancedProxy{}, 192 | httpAdvancedView: []entity.Proxy{}, 193 | httpsAdvancedView: []entity.Proxy{}, 194 | socks4AdvancedView: []entity.Proxy{}, 195 | socks5AdvancedView: []entity.Proxy{}, 196 | }, 197 | args: args{ 198 | proxy: testProxyEntity4, 199 | }, 200 | want: fields{ 201 | allClassicView: []string{ 202 | testProxy4, 203 | }, 204 | httpClassicView: []string{}, 205 | httpsClassicView: []string{}, 206 | socks4ClassicView: []string{}, 207 | socks5ClassicView: []string{ 208 | testProxy4, 209 | }, 210 | allAdvancedView: []entity.AdvancedProxy{ 211 | testAdvancedProxyEntity4, 212 | }, 213 | httpAdvancedView: []entity.Proxy{}, 214 | httpsAdvancedView: []entity.Proxy{}, 215 | socks4AdvancedView: []entity.Proxy{}, 216 | socks5AdvancedView: []entity.Proxy{ 217 | testProxyEntity4, 218 | }, 219 | }, 220 | wantErr: nil, 221 | }, 222 | { 223 | name: "DuplicatedProxyWithinHTTPCategoryAndDifferentCategory", 224 | fields: fields{ 225 | allClassicView: []string{ 226 | testProxy3, 227 | testProxy4, 228 | }, 229 | httpClassicView: []string{}, 230 | httpsClassicView: []string{}, 231 | socks4ClassicView: []string{ 232 | testProxy3, 233 | }, 234 | socks5ClassicView: []string{ 235 | testProxy4, 236 | }, 237 | allAdvancedView: []entity.AdvancedProxy{ 238 | testAdvancedProxyEntity3, 239 | testAdvancedProxyEntity4, 240 | }, 241 | httpAdvancedView: []entity.Proxy{}, 242 | httpsAdvancedView: []entity.Proxy{}, 243 | socks4AdvancedView: []entity.Proxy{ 244 | testProxyEntity3, 245 | }, 246 | socks5AdvancedView: []entity.Proxy{ 247 | testProxyEntity4, 248 | }, 249 | }, 250 | args: args{ 251 | proxy: entity.Proxy{ 252 | Category: testHTTPCategory, 253 | IP: testIP4, 254 | Port: testPort4, 255 | Proxy: testProxy4, 256 | TimeTaken: testTimeTaken, 257 | CheckedAt: testCheckedAt, 258 | }, 259 | }, 260 | want: fields{ 261 | allClassicView: []string{ 262 | testProxy3, 263 | testProxy4, 264 | }, 265 | httpClassicView: []string{ 266 | testProxy4, 267 | }, 268 | httpsClassicView: []string{}, 269 | socks4ClassicView: []string{ 270 | testProxy3, 271 | }, 272 | socks5ClassicView: []string{ 273 | testProxy4, 274 | }, 275 | allAdvancedView: []entity.AdvancedProxy{ 276 | testAdvancedProxyEntity3, 277 | { 278 | Proxy: testAdvancedProxyEntity4.Proxy, 279 | IP: testAdvancedProxyEntity4.IP, 280 | Port: testAdvancedProxyEntity4.Port, 281 | TimeTaken: testTimeTaken, 282 | CheckedAt: testAdvancedProxyEntity4.CheckedAt, 283 | Categories: []string{ 284 | testHTTPCategory, 285 | testProxyEntity4.Category, 286 | }, 287 | }, 288 | }, 289 | httpAdvancedView: []entity.Proxy{ 290 | { 291 | Category: testHTTPCategory, 292 | IP: testIP4, 293 | Port: testPort4, 294 | Proxy: testProxy4, 295 | TimeTaken: testTimeTaken, 296 | CheckedAt: testCheckedAt, 297 | }, 298 | }, 299 | httpsAdvancedView: []entity.Proxy{}, 300 | socks4AdvancedView: []entity.Proxy{ 301 | testProxyEntity3, 302 | }, 303 | socks5AdvancedView: []entity.Proxy{ 304 | testProxyEntity4, 305 | }, 306 | }, 307 | wantErr: nil, 308 | }, 309 | } 310 | 311 | for _, tt := range tests { 312 | t.Run(tt.name, func(t *testing.T) { 313 | r := &ProxyRepository{ 314 | AllClassicView: tt.fields.allClassicView, 315 | HTTPClassicView: tt.fields.httpClassicView, 316 | HTTPSClassicView: tt.fields.httpsClassicView, 317 | SOCKS4ClassicView: tt.fields.socks4ClassicView, 318 | SOCKS5ClassicView: tt.fields.socks5ClassicView, 319 | AllAdvancedView: tt.fields.allAdvancedView, 320 | HTTPAdvancedView: tt.fields.httpAdvancedView, 321 | HTTPSAdvancedView: tt.fields.httpsAdvancedView, 322 | SOCKS4AdvancedView: tt.fields.socks4AdvancedView, 323 | SOCKS5AdvancedView: tt.fields.socks5AdvancedView, 324 | } 325 | r.Store(&tt.args.proxy) 326 | 327 | views := map[string]struct { 328 | got interface{} 329 | want interface{} 330 | }{ 331 | "GetAllClassicView()": {r.AllClassicView, tt.want.allClassicView}, 332 | "GetHTTPClassicView()": {r.HTTPClassicView, tt.want.httpClassicView}, 333 | "GetHTTPSClassicView()": {r.HTTPSClassicView, tt.want.httpsClassicView}, 334 | "GetSOCKS4ClassicView()": {r.SOCKS4ClassicView, tt.want.socks4ClassicView}, 335 | "GetSOCKS5ClassicView()": {r.SOCKS5ClassicView, tt.want.socks5ClassicView}, 336 | "GetAllAdvancedView()": {r.AllAdvancedView, tt.want.allAdvancedView}, 337 | "GetHTTPAdvancedView()": {r.HTTPAdvancedView, tt.want.httpAdvancedView}, 338 | "GetHTTPSAdvancedView()": {r.HTTPSAdvancedView, tt.want.httpsAdvancedView}, 339 | "GetSOCKS4AdvancedView()": {r.SOCKS4AdvancedView, tt.want.socks4AdvancedView}, 340 | "GetSOCKS5AdvancedView()": {r.SOCKS5AdvancedView, tt.want.socks5AdvancedView}, 341 | } 342 | for name, v := range views { 343 | if !reflect.DeepEqual(v.got, v.want) { 344 | t.Errorf(expectedButGotMessage, name, v.want, v.got) 345 | } 346 | } 347 | }) 348 | } 349 | } 350 | 351 | func TestGetAllClassicView(t *testing.T) { 352 | tests := []struct { 353 | name string 354 | setup func() *ProxyRepository 355 | want []string 356 | }{ 357 | { 358 | name: "EmptyAllProxies", 359 | setup: func() *ProxyRepository { 360 | return &ProxyRepository{ 361 | AllClassicView: []string{}, 362 | } 363 | }, 364 | want: []string{}, 365 | }, 366 | { 367 | name: "WithAllProxies", 368 | setup: func() *ProxyRepository { 369 | r := &ProxyRepository{} 370 | r.AllClassicView = []string{ 371 | testProxy1, 372 | testProxy2, 373 | testProxy3, 374 | testProxy4, 375 | } 376 | return r 377 | }, 378 | want: []string{ 379 | testProxy1, 380 | testProxy2, 381 | testProxy3, 382 | testProxy4, 383 | }, 384 | }, 385 | } 386 | 387 | for _, tt := range tests { 388 | t.Run(tt.name, func(t *testing.T) { 389 | r := tt.setup() 390 | got := r.GetAllClassicView() 391 | if !reflect.DeepEqual(got, tt.want) { 392 | t.Errorf(expectedButGotMessage, "GetAllClassicView()", tt.want, got) 393 | } 394 | }) 395 | } 396 | } 397 | 398 | func TestGetHTTPClassicView(t *testing.T) { 399 | tests := []struct { 400 | name string 401 | setup func() *ProxyRepository 402 | want []string 403 | }{ 404 | { 405 | name: "EmptyHTTPProxies", 406 | setup: func() *ProxyRepository { 407 | return &ProxyRepository{ 408 | HTTPClassicView: []string{}, 409 | } 410 | }, 411 | want: []string{}, 412 | }, 413 | { 414 | name: "WithHTTPProxies", 415 | setup: func() *ProxyRepository { 416 | r := &ProxyRepository{} 417 | r.HTTPClassicView = []string{ 418 | testProxy1, 419 | } 420 | return r 421 | }, 422 | want: []string{ 423 | testProxy1, 424 | }, 425 | }, 426 | } 427 | 428 | for _, tt := range tests { 429 | t.Run(tt.name, func(t *testing.T) { 430 | r := tt.setup() 431 | got := r.GetHTTPClassicView() 432 | if !reflect.DeepEqual(got, tt.want) { 433 | t.Errorf(expectedButGotMessage, "GetHTTPClassicView()", tt.want, got) 434 | } 435 | }) 436 | } 437 | } 438 | 439 | func TestGetHTTPSClassicView(t *testing.T) { 440 | tests := []struct { 441 | name string 442 | setup func() *ProxyRepository 443 | want []string 444 | }{ 445 | { 446 | name: "EmptyHTTPSProxies", 447 | setup: func() *ProxyRepository { 448 | return &ProxyRepository{ 449 | HTTPSClassicView: []string{}, 450 | } 451 | }, 452 | want: []string{}, 453 | }, 454 | { 455 | name: "WithHTTPSProxies", 456 | setup: func() *ProxyRepository { 457 | r := &ProxyRepository{} 458 | r.HTTPSClassicView = []string{ 459 | testProxy2, 460 | } 461 | return r 462 | }, 463 | want: []string{ 464 | testProxy2, 465 | }, 466 | }, 467 | } 468 | 469 | for _, tt := range tests { 470 | t.Run(tt.name, func(t *testing.T) { 471 | r := tt.setup() 472 | got := r.GetHTTPSClassicView() 473 | if !reflect.DeepEqual(got, tt.want) { 474 | t.Errorf(expectedButGotMessage, "GetHTTPSClassicView()", tt.want, got) 475 | } 476 | }) 477 | } 478 | } 479 | 480 | func TestGetSOCKS4ClassicView(t *testing.T) { 481 | tests := []struct { 482 | name string 483 | setup func() *ProxyRepository 484 | want []string 485 | }{ 486 | { 487 | name: "EmptySOCKS4Proxies", 488 | setup: func() *ProxyRepository { 489 | r := &ProxyRepository{} 490 | r.SOCKS4ClassicView = []string{} 491 | return r 492 | }, 493 | want: []string{}, 494 | }, 495 | { 496 | name: "WithSOCKS4Proxies", 497 | setup: func() *ProxyRepository { 498 | return &ProxyRepository{ 499 | SOCKS4ClassicView: []string{ 500 | testProxy3, 501 | }, 502 | } 503 | }, 504 | want: []string{ 505 | testProxy3, 506 | }, 507 | }, 508 | } 509 | 510 | for _, tt := range tests { 511 | t.Run(tt.name, func(t *testing.T) { 512 | r := tt.setup() 513 | got := r.GetSOCKS4ClassicView() 514 | if !reflect.DeepEqual(got, tt.want) { 515 | t.Errorf(expectedButGotMessage, "GetSOCKS4ClassicView()", tt.want, got) 516 | } 517 | }) 518 | } 519 | } 520 | 521 | func TestGetSOCKS5ClassicView(t *testing.T) { 522 | tests := []struct { 523 | name string 524 | setup func() *ProxyRepository 525 | want []string 526 | }{ 527 | { 528 | name: "EmptySOCKS5Proxies", 529 | setup: func() *ProxyRepository { 530 | return &ProxyRepository{ 531 | SOCKS5ClassicView: []string{}, 532 | } 533 | }, 534 | want: []string{}, 535 | }, 536 | { 537 | name: "WithSOCKS5Proxies", 538 | setup: func() *ProxyRepository { 539 | r := &ProxyRepository{} 540 | r.SOCKS5ClassicView = []string{ 541 | testProxy4, 542 | } 543 | return r 544 | }, 545 | want: []string{ 546 | testProxy4, 547 | }, 548 | }, 549 | } 550 | 551 | for _, tt := range tests { 552 | t.Run(tt.name, func(t *testing.T) { 553 | r := tt.setup() 554 | got := r.GetSOCKS5ClassicView() 555 | if !reflect.DeepEqual(got, tt.want) { 556 | t.Errorf(expectedButGotMessage, "GetSOCKS5ClassicView()", tt.want, got) 557 | } 558 | }) 559 | } 560 | } 561 | 562 | func TestGetAllAdvancedView(t *testing.T) { 563 | tests := []struct { 564 | name string 565 | setup func() *ProxyRepository 566 | want []entity.AdvancedProxy 567 | }{ 568 | { 569 | name: "EmptyAllProxies", 570 | setup: func() *ProxyRepository { 571 | return &ProxyRepository{ 572 | AllAdvancedView: []entity.AdvancedProxy{}, 573 | } 574 | }, 575 | want: []entity.AdvancedProxy{}, 576 | }, 577 | { 578 | name: "WithAllProxies", 579 | setup: func() *ProxyRepository { 580 | r := &ProxyRepository{} 581 | r.AllAdvancedView = []entity.AdvancedProxy{ 582 | testAdvancedProxyEntity1, 583 | testAdvancedProxyEntity2, 584 | testAdvancedProxyEntity3, 585 | testAdvancedProxyEntity4, 586 | } 587 | return r 588 | }, 589 | want: []entity.AdvancedProxy{ 590 | testAdvancedProxyEntity1, 591 | testAdvancedProxyEntity2, 592 | testAdvancedProxyEntity3, 593 | testAdvancedProxyEntity4, 594 | }, 595 | }, 596 | } 597 | 598 | for _, tt := range tests { 599 | t.Run(tt.name, func(t *testing.T) { 600 | r := tt.setup() 601 | got := r.GetAllAdvancedView() 602 | if !reflect.DeepEqual(got, tt.want) { 603 | t.Errorf(expectedButGotMessage, "GetAllAdvancedView()", tt.want, got) 604 | } 605 | }) 606 | } 607 | } 608 | 609 | func TestGetHTTPAdvancedView(t *testing.T) { 610 | tests := []struct { 611 | name string 612 | setup func() *ProxyRepository 613 | want []entity.Proxy 614 | }{ 615 | { 616 | name: "EmptyHTTPProxies", 617 | setup: func() *ProxyRepository { 618 | return &ProxyRepository{ 619 | HTTPAdvancedView: []entity.Proxy{}, 620 | } 621 | }, 622 | want: []entity.Proxy{}, 623 | }, 624 | { 625 | name: "WithHTTPProxies", 626 | setup: func() *ProxyRepository { 627 | r := &ProxyRepository{} 628 | r.HTTPAdvancedView = []entity.Proxy{ 629 | testProxyEntity1, 630 | } 631 | return r 632 | }, 633 | want: []entity.Proxy{ 634 | testProxyEntity1, 635 | }, 636 | }, 637 | } 638 | 639 | for _, tt := range tests { 640 | t.Run(tt.name, func(t *testing.T) { 641 | r := tt.setup() 642 | got := r.GetHTTPAdvancedView() 643 | if !reflect.DeepEqual(got, tt.want) { 644 | t.Errorf(expectedButGotMessage, "GetHTTPAdvancedView()", tt.want, got) 645 | } 646 | }) 647 | } 648 | } 649 | 650 | func TestGetHTTPSAdvancedView(t *testing.T) { 651 | tests := []struct { 652 | name string 653 | setup func() *ProxyRepository 654 | want []entity.Proxy 655 | }{ 656 | { 657 | name: "EmptyHTTPSProxies", 658 | setup: func() *ProxyRepository { 659 | return &ProxyRepository{ 660 | HTTPSAdvancedView: []entity.Proxy{}, 661 | } 662 | }, 663 | want: []entity.Proxy{}, 664 | }, 665 | { 666 | name: "WithHTTPSProxies", 667 | setup: func() *ProxyRepository { 668 | r := &ProxyRepository{} 669 | r.HTTPSAdvancedView = []entity.Proxy{ 670 | testProxyEntity2, 671 | } 672 | return r 673 | }, 674 | want: []entity.Proxy{ 675 | testProxyEntity2, 676 | }, 677 | }, 678 | } 679 | 680 | for _, tt := range tests { 681 | t.Run(tt.name, func(t *testing.T) { 682 | r := tt.setup() 683 | got := r.GetHTTPSAdvancedView() 684 | if !reflect.DeepEqual(got, tt.want) { 685 | t.Errorf(expectedButGotMessage, "GetHTTPSAdvancedView()", tt.want, got) 686 | } 687 | }) 688 | } 689 | } 690 | 691 | func TestGetSOCKS4AdvancedView(t *testing.T) { 692 | tests := []struct { 693 | name string 694 | setup func() *ProxyRepository 695 | want []entity.Proxy 696 | }{ 697 | { 698 | name: "EmptySOCKS4Proxies", 699 | setup: func() *ProxyRepository { 700 | return &ProxyRepository{ 701 | SOCKS4AdvancedView: []entity.Proxy{}, 702 | } 703 | }, 704 | want: []entity.Proxy{}, 705 | }, 706 | { 707 | name: "WithSOCKSProxies", 708 | setup: func() *ProxyRepository { 709 | r := &ProxyRepository{} 710 | r.SOCKS4AdvancedView = []entity.Proxy{ 711 | testProxyEntity3, 712 | } 713 | return r 714 | }, 715 | want: []entity.Proxy{ 716 | testProxyEntity3, 717 | }, 718 | }, 719 | } 720 | 721 | for _, tt := range tests { 722 | t.Run(tt.name, func(t *testing.T) { 723 | r := tt.setup() 724 | got := r.GetSOCKS4AdvancedView() 725 | if !reflect.DeepEqual(got, tt.want) { 726 | t.Errorf(expectedButGotMessage, "GetSOCKS4AdvancedView()", tt.want, got) 727 | } 728 | }) 729 | } 730 | } 731 | 732 | func TestGetSOCKS5AdvancedView(t *testing.T) { 733 | tests := []struct { 734 | name string 735 | setup func() *ProxyRepository 736 | want []entity.Proxy 737 | }{ 738 | { 739 | name: "EmptySOCKS5Proxies", 740 | setup: func() *ProxyRepository { 741 | return &ProxyRepository{ 742 | SOCKS5AdvancedView: []entity.Proxy{}, 743 | } 744 | }, 745 | want: []entity.Proxy{}, 746 | }, 747 | { 748 | name: "WithSOCKS5Proxies", 749 | setup: func() *ProxyRepository { 750 | r := &ProxyRepository{} 751 | r.SOCKS5AdvancedView = []entity.Proxy{ 752 | testProxyEntity4, 753 | } 754 | return r 755 | }, 756 | want: []entity.Proxy{ 757 | testProxyEntity4, 758 | }, 759 | }, 760 | } 761 | 762 | for _, tt := range tests { 763 | t.Run(tt.name, func(t *testing.T) { 764 | r := tt.setup() 765 | got := r.GetSOCKS5AdvancedView() 766 | if !reflect.DeepEqual(got, tt.want) { 767 | t.Errorf(expectedButGotMessage, "GetSOCKS5AdvancedView()", tt.want, got) 768 | } 769 | }) 770 | } 771 | } 772 | --------------------------------------------------------------------------------