From 4d6d860316851c5a91b3f839a93bf547d457edd4 Mon Sep 17 00:00:00 2001 From: Kenichi Kamiya Date: Fri, 15 Mar 2024 17:02:26 +0900 Subject: [PATCH 1/6] Add go CI --- .github/workflows/ci-go.yml | 42 +++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 .github/workflows/ci-go.yml diff --git a/.github/workflows/ci-go.yml b/.github/workflows/ci-go.yml new file mode 100644 index 0000000..ad966d8 --- /dev/null +++ b/.github/workflows/ci-go.yml @@ -0,0 +1,42 @@ +name: CI - Go + +on: + push: + branches: + - main + paths: + - '.github/workflows/ci-go.yml' + - '**.go' + - 'go.*' + - 'testdata/**' + pull_request: + paths: + - '.github/workflows/ci-go.yml' + - '**.go' + - 'go.*' + - 'testdata/**' + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version-file: 'go.mod' + cache-dependency-path: 'go.sum' + - run: go test ./... + - run: go build -v -race ./... + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version-file: 'go.mod' + cache-dependency-path: 'go.sum' + - name: check format + run: go fmt ./... && git add --intent-to-add . && git diff --exit-code + - run: go vet ./... From e3671807d795c9839d8cd69dbf4d0cdc991e9481 Mon Sep 17 00:00:00 2001 From: Kenichi Kamiya Date: Fri, 15 Mar 2024 19:48:13 +0900 Subject: [PATCH 2/6] Update go CI --- .github/workflows/ci-go.yml | 20 +++++++++++++++----- README.md | 1 + 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci-go.yml b/.github/workflows/ci-go.yml index ad966d8..fd1e112 100644 --- a/.github/workflows/ci-go.yml +++ b/.github/workflows/ci-go.yml @@ -1,4 +1,5 @@ -name: CI - Go +# https://github.com/golang/go/issues/59968 +name: ʕ◔ϖ◔ʔ on: push: @@ -18,7 +19,7 @@ on: jobs: build: - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v4 - name: Set up Go @@ -26,10 +27,19 @@ jobs: with: go-version-file: 'go.mod' cache-dependency-path: 'go.sum' - - run: go test ./... - run: go build -v -race ./... + test: + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v4 + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version-file: 'go.mod' + cache-dependency-path: 'go.sum' + - run: go test ./... lint: - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v4 - name: Set up Go @@ -37,6 +47,6 @@ jobs: with: go-version-file: 'go.mod' cache-dependency-path: 'go.sum' + - run: go vet ./... - name: check format run: go fmt ./... && git add --intent-to-add . && git diff --exit-code - - run: go vet ./... diff --git a/README.md b/README.md index 2c5cceb..89c8d8c 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,6 @@ # gwurl - "Google distributing Windows installer"'s URL +[![CI - Go Status](https://github.com/kachick/gwurl/actions/workflows/ci-go.yml/badge.svg?branch=main)](https://github.com/kachick/gwurl/actions/workflows/ci-go.yml?query=branch%3Amain+) [![CI - Nix Status](https://github.com/kachick/gwurl/actions/workflows/ci-nix.yml/badge.svg?branch=main)](https://github.com/kachick/gwurl/actions/workflows/ci-nix.yml?query=branch%3Amain+) ## Usage From 8b36838374e181b1afcfafba844d298410d4cb16 Mon Sep 17 00:00:00 2001 From: Kenichi Kamiya Date: Fri, 15 Mar 2024 20:02:10 +0900 Subject: [PATCH 3/6] Refactor directory structure --- cmd/gwurl/main.go | 157 +++----------------------------- internal/googleapi/googleapi.go | 87 ++++++++++++++++++ internal/taggedurl/taggedurl.go | 62 +++++++++++++ 3 files changed, 160 insertions(+), 146 deletions(-) create mode 100644 internal/googleapi/googleapi.go create mode 100644 internal/taggedurl/taggedurl.go diff --git a/cmd/gwurl/main.go b/cmd/gwurl/main.go index 2df9803..2a30423 100644 --- a/cmd/gwurl/main.go +++ b/cmd/gwurl/main.go @@ -3,19 +3,13 @@ package main import ( "flag" "fmt" - "io" "log" "net/url" "os" - "path" "slices" - "strings" - "bytes" - "encoding/xml" - "net/http" - - "golang.org/x/xerrors" + "github.com/kachick/gwurl/internal/googleapi" + "github.com/kachick/gwurl/internal/taggedurl" ) var ( @@ -26,135 +20,6 @@ var ( revision = "rev" ) -type TaggedURL struct { - appguid string - ap string - appname string - needsadmin bool - filename string -} - -type Action struct { - Event string `xml:"event,attr"` - Run string `xml:"run,attr"` -} - -// NOTE: Use `InnerXML string `xml:",innerxml"“ to inspect unknown fields: https://stackoverflow.com/a/38509722 -type Response struct { - XMLName xml.Name `xml:"response"` - App struct { - Status string `xml:"status"` - UpdateCheck struct { - Manifest struct { - Version string `xml:"version,attr"` - Actions []Action `xml:"actions>action"` - } `xml:"manifest"` - - Urls []struct { - Codebase string `xml:"codebase,attr"` - } `xml:"urls>url"` - } `xml:"updatecheck"` - } `xml:"app"` -} - -type GoogleApiOs struct { - platform string - version string - architecture string -} - -type GoogleApiApp struct { - appid string - ap string -} - -func BuildGoogleApiPostXml(apiOs GoogleApiOs, apiApp GoogleApiApp) string { - return fmt.Sprintf(` - - - - - - -`, apiOs.platform, apiOs.version, apiOs.architecture, apiApp.appid, apiApp.ap) -} - -func PostGoogleAPI(apiOs GoogleApiOs, apiApp GoogleApiApp) (Response, error) { - body := []byte(BuildGoogleApiPostXml(apiOs, apiApp)) - - req, err := http.NewRequest("POST", "https://update.googleapis.com/service/update2", bytes.NewBuffer(body)) - if err != nil { - return Response{}, xerrors.Errorf("Error creating request: %w", err) - } - - req.Header.Set("Content-Type", "application/xml; charset=utf-8") - - client := &http.Client{} - resp, err := client.Do(req) - if err != nil { - return Response{}, xerrors.Errorf("Error posting request: %w", err) - } - defer resp.Body.Close() - - respBody, err := io.ReadAll(resp.Body) - if err != nil { - return Response{}, xerrors.Errorf("Error reading response body: %w", err) - } - - var responseObject Response - err = xml.Unmarshal(respBody, &responseObject) - if err != nil { - return Response{}, xerrors.Errorf("Error unmarshalling response: %w", err) - } - - return responseObject, nil -} - -func ParseTaggedURL(likeTaggedUrl string) TaggedURL { - // [scheme:][//[userinfo@]host][/]path[?query][#fragment] - taggedUrl, err := url.ParseRequestURI(likeTaggedUrl) - if err != nil { - log.Fatalf("Cannot parse given URL: %+v", err) - } - if !strings.HasSuffix(taggedUrl.Host, "google.com") { - log.Fatalf("Given URL looks not a goole: %s", taggedUrl.Host) - } - - prefixWithQuery, filename := path.Split(taggedUrl.Path) - // Intentioanlly avoiding path.Split for the getting nth element. Not the prefix and last - dirs := strings.Split(prefixWithQuery, "/") - qsi := slices.IndexFunc(dirs, func(dir string) bool { return strings.Contains(dir, "appguid") }) - qs := dirs[qsi] - query, err := url.ParseQuery(qs) - if err != nil { - log.Fatalf("Cannot parse given query: %+v", err) - } - appguid, ok := query["appguid"] - if !ok { - log.Fatalf("No appguid: %s", appguid) - } - ap, ok := query["ap"] - if !ok { - log.Fatalf("No ap: %s", ap) - } - appname, ok := query["appname"] - if !ok { - log.Fatalf("No appname: %s", ap) - } - needsadmin, ok := query["needsadmin"] - if !ok { - log.Fatalf("No needsadmin: %s", needsadmin) - } - - return TaggedURL{ - appguid: appguid[0], - ap: ap[0], - appname: appname[0], - needsadmin: needsadmin[0] == "true", - filename: filename, - } -} - func main() { versionFlag := flag.Bool("version", false, "print the version of this program") @@ -188,22 +53,22 @@ $ gwurl --version } taggedUrl := os.Args[1] - parsed := ParseTaggedURL(taggedUrl) + parsed := taggedurl.ParseTaggedURL(taggedUrl) fmt.Printf("%+v\n", parsed) - resp, err := PostGoogleAPI(GoogleApiOs{ - platform: "win", - version: "10", - architecture: "x64", - }, GoogleApiApp{ - appid: parsed.appguid, - ap: parsed.ap, + resp, err := googleapi.PostGoogleAPI(googleapi.GoogleApiOs{ + Platform: "win", + Version: "10", + Architecture: "x64", + }, googleapi.GoogleApiApp{ + Appid: parsed.Appguid, + Ap: parsed.Ap, }) if err != nil { log.Fatalf("Cannot ask to Google API: %+v", err) } - installerActionIdx := slices.IndexFunc(resp.App.UpdateCheck.Manifest.Actions, func(a Action) bool { + installerActionIdx := slices.IndexFunc(resp.App.UpdateCheck.Manifest.Actions, func(a googleapi.Action) bool { return a.Event == "install" }) installerFilename := resp.App.UpdateCheck.Manifest.Actions[installerActionIdx].Run diff --git a/internal/googleapi/googleapi.go b/internal/googleapi/googleapi.go new file mode 100644 index 0000000..36733bc --- /dev/null +++ b/internal/googleapi/googleapi.go @@ -0,0 +1,87 @@ +package googleapi + +import ( + "bytes" + "encoding/xml" + "fmt" + "io" + "net/http" + + "golang.org/x/xerrors" +) + +type Action struct { + Event string `xml:"event,attr"` + Run string `xml:"run,attr"` +} + +// NOTE: Use `InnerXML string `xml:",innerxml"“ to inspect unknown fields: https://stackoverflow.com/a/38509722 +type Response struct { + XMLName xml.Name `xml:"response"` + App struct { + Status string `xml:"status"` + UpdateCheck struct { + Manifest struct { + Version string `xml:"version,attr"` + Actions []Action `xml:"actions>action"` + } `xml:"manifest"` + + Urls []struct { + Codebase string `xml:"codebase,attr"` + } `xml:"urls>url"` + } `xml:"updatecheck"` + } `xml:"app"` +} + +type GoogleApiOs struct { + Platform string + Version string + Architecture string +} + +type GoogleApiApp struct { + Appid string + Ap string +} + +func BuildGoogleApiPostXml(apiOs GoogleApiOs, apiApp GoogleApiApp) string { + return fmt.Sprintf(` + + + + + + +`, apiOs.Platform, apiOs.Version, apiOs.Architecture, apiApp.Appid, apiApp.Ap) +} + +func PostGoogleAPI(apiOs GoogleApiOs, apiApp GoogleApiApp) (Response, error) { + body := []byte(BuildGoogleApiPostXml(apiOs, apiApp)) + + req, err := http.NewRequest("POST", "https://update.googleapis.com/service/update2", bytes.NewBuffer(body)) + if err != nil { + return Response{}, xerrors.Errorf("Error creating request: %w", err) + } + + req.Header.Set("Content-Type", "application/xml; charset=utf-8") + + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + return Response{}, xerrors.Errorf("Error posting request: %w", err) + } + defer resp.Body.Close() + + respBody, err := io.ReadAll(resp.Body) + if err != nil { + return Response{}, xerrors.Errorf("Error reading response body: %w", err) + } + + var responseObject Response + err = xml.Unmarshal(respBody, &responseObject) + if err != nil { + return Response{}, xerrors.Errorf("Error unmarshalling response: %w", err) + } + + return responseObject, nil +} diff --git a/internal/taggedurl/taggedurl.go b/internal/taggedurl/taggedurl.go new file mode 100644 index 0000000..6c99094 --- /dev/null +++ b/internal/taggedurl/taggedurl.go @@ -0,0 +1,62 @@ +package taggedurl + +import ( + "log" + "net/url" + "path" + "slices" + "strings" +) + +type TaggedURL struct { + Appguid string + Ap string + Appname string + Needsadmin bool + Filename string +} + +func ParseTaggedURL(likeTaggedUrl string) TaggedURL { + // [scheme:][//[userinfo@]host][/]path[?query][#fragment] + taggedUrl, err := url.ParseRequestURI(likeTaggedUrl) + if err != nil { + log.Fatalf("Cannot parse given URL: %+v", err) + } + if !strings.HasSuffix(taggedUrl.Host, "google.com") { + log.Fatalf("Given URL looks not a goole: %s", taggedUrl.Host) + } + + prefixWithQuery, filename := path.Split(taggedUrl.Path) + // Intentioanlly avoiding path.Split for the getting nth element. Not the prefix and last + dirs := strings.Split(prefixWithQuery, "/") + qsi := slices.IndexFunc(dirs, func(dir string) bool { return strings.Contains(dir, "appguid") }) + qs := dirs[qsi] + query, err := url.ParseQuery(qs) + if err != nil { + log.Fatalf("Cannot parse given query: %+v", err) + } + appguid, ok := query["appguid"] + if !ok { + log.Fatalf("No appguid: %s", appguid) + } + ap, ok := query["ap"] + if !ok { + log.Fatalf("No ap: %s", ap) + } + appname, ok := query["appname"] + if !ok { + log.Fatalf("No appname: %s", ap) + } + needsadmin, ok := query["needsadmin"] + if !ok { + log.Fatalf("No needsadmin: %s", needsadmin) + } + + return TaggedURL{ + Appguid: appguid[0], + Ap: ap[0], + Appname: appname[0], + Needsadmin: needsadmin[0] == "true", + Filename: filename, + } +} From ff8e2910bf89aa192a263bd4584ce1ad5ef85bd6 Mon Sep 17 00:00:00 2001 From: Kenichi Kamiya Date: Fri, 15 Mar 2024 20:30:40 +0900 Subject: [PATCH 4/6] Write parser test --- Taskfile.yml | 2 +- cmd/gwurl/main.go | 5 +- flake.nix | 2 +- go.mod | 2 + go.sum | 2 + internal/taggedurl/taggedurl.go | 21 ++++---- internal/taggedurl/taggedurl_test.go | 76 ++++++++++++++++++++++++++++ 7 files changed, 97 insertions(+), 13 deletions(-) create mode 100644 internal/taggedurl/taggedurl_test.go diff --git a/Taskfile.yml b/Taskfile.yml index a495de5..c9a2971 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -18,7 +18,7 @@ tasks: - go build ./... test: cmds: - - echo 'Update here' + - go test ./... fmt: cmds: - dprint fmt diff --git a/cmd/gwurl/main.go b/cmd/gwurl/main.go index 2a30423..5963a7d 100644 --- a/cmd/gwurl/main.go +++ b/cmd/gwurl/main.go @@ -53,7 +53,10 @@ $ gwurl --version } taggedUrl := os.Args[1] - parsed := taggedurl.ParseTaggedURL(taggedUrl) + parsed, err := taggedurl.ParseTaggedURL(taggedUrl) + if err != nil { + log.Fatalf("Cannot parse given URL: %+v", err) + } fmt.Printf("%+v\n", parsed) resp, err := googleapi.PostGoogleAPI(googleapi.GoogleApiOs{ diff --git a/flake.nix b/flake.nix index ff02635..7c94668 100644 --- a/flake.nix +++ b/flake.nix @@ -43,7 +43,7 @@ # When updating go.mod or go.sum, update this sha together as following # vendorHash = "sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="; # (`pkgs.lib.fakeSha256` returns invalid string in thesedays... :<) - vendorHash = "sha256-sODHIjL/iaWzH0iarh8Y9N7hZGKznbUxwE5xOPwEFvc="; + vendorHash = "sha256-hC1eg2mC3Qp0QGFj3pTMIOyjrMV9Yx+hqvupxUG17OQ="; }; packages.default = packages.gwurl; diff --git a/go.mod b/go.mod index 0fb6aa8..d337e31 100644 --- a/go.mod +++ b/go.mod @@ -3,3 +3,5 @@ module github.com/kachick/gwurl go 1.22.1 require golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 + +require github.com/google/go-cmp v0.6.0 // indirect diff --git a/go.sum b/go.sum index a3cee59..35173c2 100644 --- a/go.sum +++ b/go.sum @@ -1,2 +1,4 @@ +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 h1:+cNy6SZtPcJQH3LJVLOSmiC7MMxXNOb3PU/VUEz+EhU= golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= diff --git a/internal/taggedurl/taggedurl.go b/internal/taggedurl/taggedurl.go index 6c99094..7ab612b 100644 --- a/internal/taggedurl/taggedurl.go +++ b/internal/taggedurl/taggedurl.go @@ -1,11 +1,12 @@ package taggedurl import ( - "log" "net/url" "path" "slices" "strings" + + "golang.org/x/xerrors" ) type TaggedURL struct { @@ -16,14 +17,14 @@ type TaggedURL struct { Filename string } -func ParseTaggedURL(likeTaggedUrl string) TaggedURL { +func ParseTaggedURL(likeTaggedUrl string) (TaggedURL, error) { // [scheme:][//[userinfo@]host][/]path[?query][#fragment] taggedUrl, err := url.ParseRequestURI(likeTaggedUrl) if err != nil { - log.Fatalf("Cannot parse given URL: %+v", err) + return TaggedURL{}, xerrors.Errorf("Cannot parse given URL: %w", err) } if !strings.HasSuffix(taggedUrl.Host, "google.com") { - log.Fatalf("Given URL looks not a goole: %s", taggedUrl.Host) + return TaggedURL{}, xerrors.Errorf("Given URL looks not a goole: %s", taggedUrl.Host) } prefixWithQuery, filename := path.Split(taggedUrl.Path) @@ -33,23 +34,23 @@ func ParseTaggedURL(likeTaggedUrl string) TaggedURL { qs := dirs[qsi] query, err := url.ParseQuery(qs) if err != nil { - log.Fatalf("Cannot parse given query: %+v", err) + return TaggedURL{}, xerrors.Errorf("Cannot parse given query: %w", err) } appguid, ok := query["appguid"] if !ok { - log.Fatalf("No appguid: %s", appguid) + return TaggedURL{}, xerrors.Errorf("No appguid: %s", appguid) } ap, ok := query["ap"] if !ok { - log.Fatalf("No ap: %s", ap) + return TaggedURL{}, xerrors.Errorf("No ap: %s", ap) } appname, ok := query["appname"] if !ok { - log.Fatalf("No appname: %s", ap) + return TaggedURL{}, xerrors.Errorf("No appname: %s", appname) } needsadmin, ok := query["needsadmin"] if !ok { - log.Fatalf("No needsadmin: %s", needsadmin) + return TaggedURL{}, xerrors.Errorf("No needsadmin: %s", needsadmin) } return TaggedURL{ @@ -58,5 +59,5 @@ func ParseTaggedURL(likeTaggedUrl string) TaggedURL { Appname: appname[0], Needsadmin: needsadmin[0] == "true", Filename: filename, - } + }, nil } diff --git a/internal/taggedurl/taggedurl_test.go b/internal/taggedurl/taggedurl_test.go new file mode 100644 index 0000000..ff3f09b --- /dev/null +++ b/internal/taggedurl/taggedurl_test.go @@ -0,0 +1,76 @@ +package taggedurl + +import ( + "testing" + + "github.com/google/go-cmp/cmp" +) + +func TestParseTaggedURL(t *testing.T) { + testCases := []struct { + description string + input string + want TaggedURL + ok bool + }{ + { + description: "Google Japanese IME", + input: "https://dl.google.com/tag/s/appguid%3D%7BDDCCD2A9-025E-4142-BCEB-F467B88CF830%7D%26iid%3D%7BBF6725E7-4A15-87B7-24D5-0ADABEC753EE%7D%26lang%3Dja%26browser%3D4%26usagestats%3D0%26appname%3DGoogle%2520%25E6%2597%25A5%25E6%259C%25AC%25E8%25AA%259E%25E5%2585%25A5%25E5%258A%259B%26needsadmin%3Dtrue%26ap%3Dexternal-stable-universal/japanese-ime/GoogleJapaneseInputSetup.exe", + want: TaggedURL{ + Appguid: "{DDCCD2A9-025E-4142-BCEB-F467B88CF830}", + Ap: "external-stable-universal", + Appname: "Google 日本語入力", + Needsadmin: true, + Filename: "GoogleJapaneseInputSetup.exe", + }, + ok: true, + }, + { + description: "Google Chrome", + input: "https://dl.google.com/tag/s/appguid%3D%7B8A69D345-D564-463C-AFF1-A69D9E530F96%7D%26iid%3D%7BF99B66DF-85A3-C043-7205-7917C2AA7AAB%7D%26lang%3Dja%26browser%3D4%26usagestats%3D1%26appname%3DGoogle%2520Chrome%26needsadmin%3Dprefers%26ap%3Dx64-stable-statsdef_1%26installdataindex%3Dempty/update2/installers/ChromeSetup.exe", + want: TaggedURL{ + Appguid: "{8A69D345-D564-463C-AFF1-A69D9E530F96}", + Ap: "x64-stable-statsdef_1", + Appname: "Google Chrome", + Needsadmin: false, + Filename: "ChromeSetup.exe", + }, + ok: true, + }, + { + description: "Unknown URL", + input: "https://example.cpm/dir/foobar.exe", + want: TaggedURL{}, + ok: false, + }, + { + description: "Not a URL", + input: ":)", + want: TaggedURL{}, + ok: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.description, func(t *testing.T) { + parsed, err := ParseTaggedURL(tc.input) + + if err != nil { + if tc.ok { + t.Errorf("unexpected error happned: %v", err) + } else { + return + } + } + + if !tc.ok { + t.Errorf("expected error did not happen") + return + } + + if diff := cmp.Diff(tc.want, parsed); diff != "" { + t.Errorf("wrong result: %s", diff) + } + }) + } +} From 977c9e089350e79799b169de38e81e925ef82494 Mon Sep 17 00:00:00 2001 From: Kenichi Kamiya Date: Fri, 15 Mar 2024 21:31:57 +0900 Subject: [PATCH 5/6] Write E2E test including api call --- .github/workflows/ci-go.yml | 1 + .github/workflows/ci-googleapi.yml | 21 ++++++ .vscode/settings.json | 3 + README.md | 1 + Taskfile.yml | 3 + internal/googleapi/googleapi.go | 22 ++++++ internal/googleapi/googleapi_test.go | 104 +++++++++++++++++++++++++++ 7 files changed, 155 insertions(+) create mode 100644 .github/workflows/ci-googleapi.yml create mode 100644 internal/googleapi/googleapi_test.go diff --git a/.github/workflows/ci-go.yml b/.github/workflows/ci-go.yml index fd1e112..fdd4e6d 100644 --- a/.github/workflows/ci-go.yml +++ b/.github/workflows/ci-go.yml @@ -16,6 +16,7 @@ on: - '**.go' - 'go.*' - 'testdata/**' + workflow_dispatch: jobs: build: diff --git a/.github/workflows/ci-googleapi.yml b/.github/workflows/ci-googleapi.yml new file mode 100644 index 0000000..8752405 --- /dev/null +++ b/.github/workflows/ci-googleapi.yml @@ -0,0 +1,21 @@ +name: 📡 + +on: + # This test actually send request to Google API, so do not add frequently triggers as push/PR + schedule: + # Every 10:42 JST + # https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#schedule + - cron: '42 1 * * *' + workflow_dispatch: + +jobs: + test-actual-api: + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v4 + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version-file: 'go.mod' + cache-dependency-path: 'go.sum' + - run: go test -tags=apitest ./... diff --git a/.vscode/settings.json b/.vscode/settings.json index 81edc8c..6207fdb 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -15,5 +15,8 @@ "command": ["nixpkgs-fmt"] } } + }, + "gopls": { + "build.buildFlags": ["-tags=apitest"] } } diff --git a/README.md b/README.md index 89c8d8c..7b562bc 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ # gwurl - "Google distributing Windows installer"'s URL [![CI - Go Status](https://github.com/kachick/gwurl/actions/workflows/ci-go.yml/badge.svg?branch=main)](https://github.com/kachick/gwurl/actions/workflows/ci-go.yml?query=branch%3Amain+) +[![CI - E2E Status](https://github.com/kachick/gwurl/actions/workflows/ci-googleapi.yml/badge.svg?branch=main)](https://github.com/kachick/gwurl/actions/workflows/ci-googleapi.yml?query=branch%3Amain+) [![CI - Nix Status](https://github.com/kachick/gwurl/actions/workflows/ci-nix.yml/badge.svg?branch=main)](https://github.com/kachick/gwurl/actions/workflows/ci-nix.yml?query=branch%3Amain+) ## Usage diff --git a/Taskfile.yml b/Taskfile.yml index c9a2971..768bfab 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -19,6 +19,9 @@ tasks: test: cmds: - go test ./... + test-all: + cmds: + - go test -tags=apitest ./... fmt: cmds: - dprint fmt diff --git a/internal/googleapi/googleapi.go b/internal/googleapi/googleapi.go index 36733bc..922bb3e 100644 --- a/internal/googleapi/googleapi.go +++ b/internal/googleapi/googleapi.go @@ -6,6 +6,8 @@ import ( "fmt" "io" "net/http" + "net/url" + "slices" "golang.org/x/xerrors" ) @@ -85,3 +87,23 @@ func PostGoogleAPI(apiOs GoogleApiOs, apiApp GoogleApiApp) (Response, error) { return responseObject, nil } + +func GetPermalinks(resp Response) ([]string, error) { + installerActionIdx := slices.IndexFunc(resp.App.UpdateCheck.Manifest.Actions, func(a Action) bool { + return a.Event == "install" + }) + if installerActionIdx < 0 { + return nil, xerrors.Errorf("api didn't return installer information: %v", installerActionIdx) + } + installerFilename := resp.App.UpdateCheck.Manifest.Actions[installerActionIdx].Run + + permalinks := make([]string, 0, len(resp.App.UpdateCheck.Urls)) + for _, u := range resp.App.UpdateCheck.Urls { + permalink, err := url.JoinPath(u.Codebase, installerFilename) + if err != nil { + return nil, xerrors.Errorf("Cannot build final link with the result: %w", err) + } + permalinks = append(permalinks, permalink) + } + return permalinks, nil +} diff --git a/internal/googleapi/googleapi_test.go b/internal/googleapi/googleapi_test.go new file mode 100644 index 0000000..4dc8e7a --- /dev/null +++ b/internal/googleapi/googleapi_test.go @@ -0,0 +1,104 @@ +//go:build apitest +// +build apitest + +package googleapi + +import ( + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" +) + +func TestPostGoogleAPI(t *testing.T) { + testCases := []struct { + description string + input GoogleApiApp + want []string + ok bool + }{ + { + description: "Google Japanese IME", + input: GoogleApiApp{ + Appid: "{DDCCD2A9-025E-4142-BCEB-F467B88CF830}", + Ap: "external-stable-universal", + }, + want: []string{ + "http://edgedl.me.gvt1.com/edgedl/release2/kjspmop3m4hu2sbbaotsynsgja_2.29.5370.0/GoogleJapaneseInput64-2.29.5370.0.msi", + "https://edgedl.me.gvt1.com/edgedl/release2/kjspmop3m4hu2sbbaotsynsgja_2.29.5370.0/GoogleJapaneseInput64-2.29.5370.0.msi", + "http://dl.google.com/release2/kjspmop3m4hu2sbbaotsynsgja_2.29.5370.0/GoogleJapaneseInput64-2.29.5370.0.msi", + "https://dl.google.com/release2/kjspmop3m4hu2sbbaotsynsgja_2.29.5370.0/GoogleJapaneseInput64-2.29.5370.0.msi", + "http://www.google.com/dl/release2/kjspmop3m4hu2sbbaotsynsgja_2.29.5370.0/GoogleJapaneseInput64-2.29.5370.0.msi", + "https://www.google.com/dl/release2/kjspmop3m4hu2sbbaotsynsgja_2.29.5370.0/GoogleJapaneseInput64-2.29.5370.0.msi", + }, + ok: true, + }, + { + description: "Google Chrome", + input: GoogleApiApp{ + Appid: "{8A69D345-D564-463C-AFF1-A69D9E530F96}", + Ap: "x64-stable-statsdef_1", + }, + want: []string{ + "http://edgedl.me.gvt1.com/edgedl/release2/chrome/adno2uyj7yhsdmrqsizskbq3um2q_122.0.6261.129/122.0.6261.129_chrome_installer.exe", + "https://edgedl.me.gvt1.com/edgedl/release2/chrome/adno2uyj7yhsdmrqsizskbq3um2q_122.0.6261.129/122.0.6261.129_chrome_installer.exe", + "http://dl.google.com/release2/chrome/adno2uyj7yhsdmrqsizskbq3um2q_122.0.6261.129/122.0.6261.129_chrome_installer.exe", + "https://dl.google.com/release2/chrome/adno2uyj7yhsdmrqsizskbq3um2q_122.0.6261.129/122.0.6261.129_chrome_installer.exe", + "http://www.google.com/dl/release2/chrome/adno2uyj7yhsdmrqsizskbq3um2q_122.0.6261.129/122.0.6261.129_chrome_installer.exe", + "https://www.google.com/dl/release2/chrome/adno2uyj7yhsdmrqsizskbq3um2q_122.0.6261.129/122.0.6261.129_chrome_installer.exe", + }, + ok: true, + }, + { + description: "Unknown Prams", + input: GoogleApiApp{ + Appid: "foo", + Ap: "bar", + }, + want: nil, + ok: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.description, func(t *testing.T) { + resp, err := PostGoogleAPI(GoogleApiOs{ + Platform: "win", + Version: "10", + Architecture: "x64", + }, tc.input) + if err != nil { + if tc.ok { + t.Errorf("unexpected error happned: %v", err) + return + } else { + return + } + } + + urls, err := GetPermalinks(resp) + if err != nil { + if tc.ok { + t.Errorf("unexpected error happned: %v", err) + return + } else { + return + } + } + + if !tc.ok { + t.Errorf("expected error did not happen") + return + } + + dictComp := func(a string, b string) bool { + return strings.Compare(a, b) == -1 + } + + if diff := cmp.Diff(tc.want, urls, cmpopts.SortSlices(dictComp)); diff != "" { + t.Errorf("wrong result: %s", diff) + } + }) + } +} From c5692bf09bca186c66b555aed856a44cd9d4b4ba Mon Sep 17 00:00:00 2001 From: Kenichi Kamiya Date: Fri, 15 Mar 2024 21:44:49 +0900 Subject: [PATCH 6/6] Trigger apitest with labels --- .github/workflows/ci-go.yml | 2 -- .github/workflows/ci-googleapi.yml | 15 ++++++++++++++- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci-go.yml b/.github/workflows/ci-go.yml index fdd4e6d..1d63591 100644 --- a/.github/workflows/ci-go.yml +++ b/.github/workflows/ci-go.yml @@ -9,13 +9,11 @@ on: - '.github/workflows/ci-go.yml' - '**.go' - 'go.*' - - 'testdata/**' pull_request: paths: - '.github/workflows/ci-go.yml' - '**.go' - 'go.*' - - 'testdata/**' workflow_dispatch: jobs: diff --git a/.github/workflows/ci-googleapi.yml b/.github/workflows/ci-googleapi.yml index 8752405..5d72e14 100644 --- a/.github/workflows/ci-googleapi.yml +++ b/.github/workflows/ci-googleapi.yml @@ -1,7 +1,16 @@ name: 📡 on: - # This test actually send request to Google API, so do not add frequently triggers as push/PR + push: + branches: + - main + paths: + - '.github/workflows/ci-go.yml' + - '**.go' + - 'go.*' + pull_request: + types: + - labeled schedule: # Every 10:42 JST # https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#schedule @@ -10,6 +19,10 @@ on: jobs: test-actual-api: + # This test actually send request to Google API, so prevent frequently triggers with the labeled or not + if: >- + (github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'apitest')) || + (github.event_name != 'pull_request') runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v4