golang.org/x/build@v0.0.0-20240506185731-218518f32b70/internal/task/tweet_test.go (about)

     1  // Copyright 2021 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  package task
     6  
     7  import (
     8  	"bytes"
     9  	"context"
    10  	"encoding/json"
    11  	"flag"
    12  	"fmt"
    13  	"image"
    14  	"image/png"
    15  	"io"
    16  	"net/http"
    17  	"net/http/httptest"
    18  	"os"
    19  	"path/filepath"
    20  	"reflect"
    21  	"strings"
    22  	"testing"
    23  
    24  	"github.com/google/go-cmp/cmp"
    25  	"golang.org/x/build/buildenv"
    26  	"golang.org/x/build/internal/secret"
    27  	"golang.org/x/build/internal/workflow"
    28  )
    29  
    30  var postTests = [...]struct {
    31  	name         string
    32  	kind         ReleaseKind
    33  	published    []Published
    34  	security     string
    35  	announcement string
    36  	randomSeed   int64
    37  	wantLog      string
    38  }{
    39  	{
    40  		name: "minor",
    41  		kind: KindMinor,
    42  		published: []Published{
    43  			{Version: "go1.17.1", Files: []WebsiteFile{{
    44  				OS: "linux", Arch: "arm64",
    45  				Filename: "go1.17.1.linux-arm64.tar.gz", Size: 102606384, Kind: "archive"}},
    46  			},
    47  			{Version: "go1.16.8"},
    48  		},
    49  		security:     "Includes security fixes for A and B.",
    50  		announcement: "https://groups.google.com/g/golang-announce/c/dx9d7IOseHw/m/KNH37k37AAAJ",
    51  		randomSeed:   234,
    52  		wantLog: `tweet text:
    53  🎊 Go 1.17.1 and 1.16.8 are released!
    54  
    55  🔐 Security: Includes security fixes for A and B.
    56  
    57  📢 Announcement: https://groups.google.com/g/golang-announce/c/dx9d7IOseHw/m/KNH37k37AAAJ
    58  
    59  ⬇️ Download: https://go.dev/dl/#go1.17.1
    60  
    61  #golang
    62  tweet image:
    63  $ go install golang.org/dl/go1.17.1@latest
    64  $ go1.17.1 download
    65  Downloaded   0.0% (        0 / 102606384 bytes) ...
    66  Downloaded  50.0% ( 51303192 / 102606384 bytes) ...
    67  Downloaded 100.0% (102606384 / 102606384 bytes)
    68  Unpacking go1.17.1.linux-arm64.tar.gz ...
    69  Success. You may now run 'go1.17.1'
    70  $ go1.17.1 version
    71  go version go1.17.1 linux/arm64` + "\n",
    72  	},
    73  	{
    74  		name: "minor-solo",
    75  		kind: KindMinor,
    76  		published: []Published{{Version: "go1.11.1", Files: []WebsiteFile{{
    77  			OS: "darwin", Arch: "amd64",
    78  			Filename: "go1.11.1.darwin-amd64.tar.gz", Size: 124181190, Kind: "archive"}},
    79  		}},
    80  		announcement: "https://groups.google.com/g/golang-announce/c/pFXKAfoVJqw",
    81  		randomSeed:   23,
    82  		wantLog: `tweet text:
    83  🎆 Go 1.11.1 is released!
    84  
    85  📣 Announcement: https://groups.google.com/g/golang-announce/c/pFXKAfoVJqw
    86  
    87  📦 Download: https://go.dev/dl/#go1.11.1
    88  
    89  #golang
    90  tweet image:
    91  $ go install golang.org/dl/go1.11.1@latest
    92  $ go1.11.1 download
    93  Downloaded   0.0% (        0 / 124181190 bytes) ...
    94  Downloaded  50.0% ( 62090595 / 124181190 bytes) ...
    95  Downloaded 100.0% (124181190 / 124181190 bytes)
    96  Unpacking go1.11.1.darwin-amd64.tar.gz ...
    97  Success. You may now run 'go1.11.1'
    98  $ go1.11.1 version
    99  go version go1.11.1 darwin/amd64` + "\n",
   100  	},
   101  	{
   102  		name: "beta",
   103  		kind: KindBeta,
   104  		published: []Published{{Version: "go1.17beta1", Files: []WebsiteFile{{
   105  			OS: "darwin", Arch: "amd64",
   106  			Filename: "go1.17beta1.darwin-amd64.tar.gz", Size: 135610703, Kind: "archive"}},
   107  		}},
   108  		announcement: "https://groups.google.com/g/golang-announce/c/i4EliPDV9Ok/m/MxA-nj53AAAJ",
   109  		randomSeed:   678,
   110  		wantLog: `tweet text:
   111  ⚡️ Go 1.17 Beta 1 is released!
   112  
   113  ⚙️ Try it! File bugs! https://go.dev/issue/new
   114  
   115  🗣 Announcement: https://groups.google.com/g/golang-announce/c/i4EliPDV9Ok/m/MxA-nj53AAAJ
   116  
   117  📦 Download: https://go.dev/dl/#go1.17beta1
   118  
   119  #golang
   120  tweet image:
   121  $ go install golang.org/dl/go1.17beta1@latest
   122  $ go1.17beta1 download
   123  Downloaded   0.0% (        0 / 135610703 bytes) ...
   124  Downloaded  50.0% ( 67805351 / 135610703 bytes) ...
   125  Downloaded 100.0% (135610703 / 135610703 bytes)
   126  Unpacking go1.17beta1.darwin-amd64.tar.gz ...
   127  Success. You may now run 'go1.17beta1'
   128  $ go1.17beta1 version
   129  go version go1.17beta1 darwin/amd64` + "\n",
   130  	},
   131  	{
   132  		name: "rc",
   133  		kind: KindRC,
   134  		published: []Published{{Version: "go1.17rc2", Files: []WebsiteFile{{
   135  			OS: "windows", Arch: "arm64",
   136  			Filename: "go1.17rc2.windows-arm64.zip", Size: 116660997, Kind: "archive"}},
   137  		}},
   138  		announcement: "https://groups.google.com/g/golang-announce/c/yk30ovJGXWY/m/p9uUnKbbBQAJ",
   139  		randomSeed:   456,
   140  		wantLog: `tweet text:
   141  🎉 Go 1.17 Release Candidate 2 is released!
   142  
   143  🏖 Run it in dev! Run it in prod! File bugs! https://go.dev/issue/new
   144  
   145  🔈 Announcement: https://groups.google.com/g/golang-announce/c/yk30ovJGXWY/m/p9uUnKbbBQAJ
   146  
   147  📦 Download: https://go.dev/dl/#go1.17rc2
   148  
   149  #golang
   150  tweet image:
   151  $ go install golang.org/dl/go1.17rc2@latest
   152  $ go1.17rc2 download
   153  Downloaded   0.0% (        0 / 116660997 bytes) ...
   154  Downloaded  50.0% ( 58330498 / 116660997 bytes) ...
   155  Downloaded 100.0% (116660997 / 116660997 bytes)
   156  Unpacking go1.17rc2.windows-arm64.zip ...
   157  Success. You may now run 'go1.17rc2'
   158  $ go1.17rc2 version
   159  go version go1.17rc2 windows/arm64` + "\n",
   160  	},
   161  	{
   162  		name: "major",
   163  		kind: KindMajor,
   164  		published: []Published{{Version: "go1.21.0", Files: []WebsiteFile{{
   165  			OS: "freebsd", Arch: "amd64",
   166  			Filename: "go1.21.0.freebsd-amd64.tar.gz", Size: 133579378, Kind: "archive"}},
   167  		}},
   168  		security:   "Includes a super duper security fix (CVE-123).",
   169  		randomSeed: 123,
   170  		wantLog: `tweet text:
   171  🥳 Go 1.21.0 is released!
   172  
   173  🔐 Security: Includes a super duper security fix (CVE-123).
   174  
   175  📝 Release notes: https://go.dev/doc/go1.21
   176  
   177  📦 Download: https://go.dev/dl/#go1.21.0
   178  
   179  #golang
   180  tweet image:
   181  $ go install golang.org/dl/go1.21.0@latest
   182  $ go1.21.0 download
   183  Downloaded   0.0% (        0 / 133579378 bytes) ...
   184  Downloaded  50.0% ( 66789689 / 133579378 bytes) ...
   185  Downloaded 100.0% (133579378 / 133579378 bytes)
   186  Unpacking go1.21.0.freebsd-amd64.tar.gz ...
   187  Success. You may now run 'go1.21.0'
   188  $ go1.21.0 version
   189  go version go1.21.0 freebsd/amd64` + "\n",
   190  	},
   191  }
   192  
   193  func TestTweetRelease(t *testing.T) {
   194  	for _, tc := range postTests {
   195  		t.Run(tc.name, func(t *testing.T) {
   196  			// Call the tweet task function in dry-run mode so it
   197  			// doesn't actually try to tweet, but capture its log.
   198  			var buf bytes.Buffer
   199  			ctx := &workflow.TaskContext{Context: context.Background(), Logger: fmtWriter{&buf}}
   200  			tweetURL, err := (SocialMediaTasks{RandomSeed: tc.randomSeed}).TweetRelease(ctx, tc.kind, tc.published, tc.security, tc.announcement)
   201  			if err != nil {
   202  				t.Fatal("got a non-nil error:", err)
   203  			}
   204  			if got, want := tweetURL, "(dry-run)"; got != want {
   205  				t.Errorf("unexpected tweetURL: got = %q, want %q", got, want)
   206  			}
   207  			if diff := cmp.Diff(tc.wantLog, buf.String()); diff != "" {
   208  				t.Errorf("log mismatch (-want +got):\n%s", diff)
   209  			}
   210  		})
   211  	}
   212  }
   213  
   214  type fmtWriter struct{ w io.Writer }
   215  
   216  func (f fmtWriter) Printf(format string, v ...interface{}) {
   217  	fmt.Fprintf(f.w, format, v...)
   218  }
   219  
   220  var mastodonAPI secret.MastodonCredentials
   221  var secretErr error
   222  var mastodonPMTarget = flag.String("mastodon", "", "Name of account to receive private message (e.g., @user@instance.suffix)")
   223  
   224  func init() {
   225  	secretErr = secret.InitFlagSupport(context.Background())
   226  	secret.JSONVarFlag(&mastodonAPI, "mastodon-api-secret", "Mastodon API secret to use for tests that post.")
   227  	flag.Set("mastodon-api-secret", fmt.Sprintf("secret:%s/%s", buildenv.Production.ProjectName, secret.NameMastodonAPISecret))
   228  }
   229  
   230  var updateFlag = flag.Bool("update", false, "Update golden files.")
   231  
   232  func TestDrawTerminal(t *testing.T) {
   233  	got, err := drawTerminal(`$ go install golang.org/dl/go1.18beta1@latest
   234  $ go1.18beta1 download
   235  Downloaded   0.0% (        0 / 111109966 bytes) ...
   236  Downloaded  50.0% ( 55554983 / 111109966 bytes) ...
   237  Downloaded 100.0% (111109966 / 111109966 bytes)
   238  Unpacking go1.18beta1.linux-s390x.tar.gz ...
   239  Success. You may now run 'go1.18beta1'
   240  $ go1.18beta1 version
   241  go version go1.18beta1 linux/s390x`)
   242  	if err != nil {
   243  		t.Fatalf("drawTerminal: got error=%v, want nil", err)
   244  	}
   245  	if *updateFlag {
   246  		encodePNG(t, filepath.Join("testdata", "terminal.png"), got)
   247  		return
   248  	}
   249  	want := decodePNG(t, filepath.Join("testdata", "terminal.png"))
   250  	if !got.Bounds().Eq(want.Bounds()) {
   251  		t.Fatalf("drawTerminal: got image bounds=%v, want %v", got.Bounds(), want.Bounds())
   252  	}
   253  	diff := func(a, b uint32) uint64 {
   254  		if a < b {
   255  			return uint64(b - a)
   256  		}
   257  		return uint64(a - b)
   258  	}
   259  	var total uint64
   260  	for y := 0; y < want.Bounds().Dy(); y++ {
   261  		for x := 0; x < want.Bounds().Dx(); x++ {
   262  			r0, g0, b0, a0 := got.At(x, y).RGBA()
   263  			r1, g1, b1, a1 := want.At(x, y).RGBA()
   264  			const D = 0xffff * 20 / 100 // Diff threshold of 20% for RGB color components.
   265  			if diff(r0, r1) > D || diff(g0, g1) > D || diff(b0, b1) > D || a0 != a1 {
   266  				t.Errorf("at (%d, %d):\n got RGBA %v\nwant RGBA %v", x, y, got.At(x, y), want.At(x, y))
   267  			}
   268  			total += diff(r0, r1) + diff(g0, g1) + diff(b0, b1)
   269  		}
   270  	}
   271  	if testing.Verbose() {
   272  		t.Logf("average pixel color diff: %v%%", 100*float64(total)/float64(0xffff*want.Bounds().Dx()*want.Bounds().Dy()))
   273  	}
   274  }
   275  
   276  func encodePNG(t *testing.T, name string, m image.Image) {
   277  	t.Helper()
   278  	var buf bytes.Buffer
   279  	err := (&png.Encoder{CompressionLevel: png.BestCompression}).Encode(&buf, m)
   280  	if err != nil {
   281  		t.Fatal(err)
   282  	}
   283  	err = os.WriteFile(name, buf.Bytes(), 0644)
   284  	if err != nil {
   285  		t.Fatal(err)
   286  	}
   287  }
   288  
   289  func decodePNG(t *testing.T, name string) image.Image {
   290  	t.Helper()
   291  	f, err := os.Open(name)
   292  	if err != nil {
   293  		t.Fatal(err)
   294  	}
   295  	defer f.Close()
   296  	m, err := png.Decode(f)
   297  	if err != nil {
   298  		t.Fatal(err)
   299  	}
   300  	return m
   301  }
   302  
   303  // TestPostToMastodonUsingCredentials always passes unless some invariant is badly wrong.
   304  // This is intended to allow as-close-to-real testing as possible.
   305  // It is capable of actual activity on Mastodon, this will be a DM
   306  // to a specified person, so don't get cute with your example recipient.
   307  // DO NOT AUTOMATE THIS TEST, IT SHOULD BE RUN BY A HUMAN.
   308  func TestPostToMastodonUsingCredentials(t *testing.T) {
   309  	pmTarget := strings.TrimSpace(*mastodonPMTarget)
   310  	t.Logf("private message target (-mastodon)=%v", pmTarget)
   311  	t.Logf("mastodonAPI=%v", mastodonAPI)
   312  	if pmTarget == "" {
   313  		t.Skipf("Nothing to do here without a '-mastodon' flag")
   314  	}
   315  	if secretErr != nil {
   316  		t.Skipf("Nothing to do here without access to secrets, err=%v", secretErr)
   317  	}
   318  	if mastodonAPI.Application == "" {
   319  		t.Skipf("Nothing to do here without a valid API key")
   320  	}
   321  
   322  	cl, err := NewTestMastodonClient(mastodonAPI, pmTarget)
   323  	if err != nil {
   324  		t.Fatalf("NewTestMastodonClient(%v, %s) error %v", mastodonAPI, pmTarget, err)
   325  	}
   326  
   327  	tc := &postTests[0]
   328  
   329  	var buf bytes.Buffer
   330  	ctx := &workflow.TaskContext{Context: context.Background(), Logger: fmtWriter{&buf}}
   331  	tweetURL, err := (SocialMediaTasks{RandomSeed: tc.randomSeed, MastodonClient: cl}).TrumpetRelease(ctx, tc.kind, tc.published, tc.security, tc.announcement)
   332  	t.Logf("Mastodon post URL=%v, err=%v", tweetURL, err)
   333  	t.Log(buf.String())
   334  }
   335  
   336  func TestPostTweet(t *testing.T) {
   337  	mux := http.NewServeMux()
   338  	mux.HandleFunc("upload.twitter.com/1.1/media/upload.json", func(w http.ResponseWriter, req *http.Request) {
   339  		if got, want := req.Method, http.MethodPost; got != want {
   340  			t.Errorf("/1.1/media/upload.json: got method %s, want %s", got, want)
   341  			return
   342  		}
   343  		if got, want := req.FormValue("media_category"), "tweet_image"; got != want {
   344  			t.Errorf("/1.1/media/upload.json: got media_category=%q, want %q", got, want)
   345  		}
   346  		f, hdr, err := req.FormFile("media")
   347  		if err != nil {
   348  			t.Errorf("/1.1/media/upload.json: error getting image file: %v", err)
   349  			return
   350  		}
   351  		if got, want := hdr.Filename, "image.png"; got != want {
   352  			t.Errorf("/1.1/media/upload.json: got file name=%q, want %q", got, want)
   353  		}
   354  		if got, want := mustRead(f), "image-png-bytes"; got != want {
   355  			t.Errorf("/1.1/media/upload.json: got file content=%q, want %q", got, want)
   356  			return
   357  		}
   358  		mustWrite(w, `{"media_id_string": "media-123"}`)
   359  	})
   360  	mux.HandleFunc("api.twitter.com/2/tweets", func(w http.ResponseWriter, req *http.Request) {
   361  		if got, want := req.Method, http.MethodPost; got != want {
   362  			t.Errorf("/2/tweets: got method %s, want %s", got, want)
   363  			return
   364  		}
   365  		if got, want := req.Header.Get("Content-Type"), "application/json"; got != want {
   366  			t.Errorf("/2/tweets: got Content-Type=%q, want %q", got, want)
   367  			return
   368  		}
   369  		var v struct {
   370  			Text  string `json:"text"`
   371  			Media struct {
   372  				MediaIDs []string `json:"media_ids"`
   373  			} `json:"media"`
   374  		}
   375  		if err := json.NewDecoder(req.Body).Decode(&v); err != nil {
   376  			t.Errorf("/2/tweets: decode JSON error: %v", err)
   377  			return
   378  		}
   379  		if got, want := v.Text, "tweet-text"; got != want {
   380  			t.Errorf("/2/tweets: got status=%q, want %q", got, want)
   381  		}
   382  		if got, want := v.Media.MediaIDs, []string{"media-123"}; !reflect.DeepEqual(got, want) {
   383  			t.Errorf("/2/tweets: got media_ids=%q, want %q", got, want)
   384  		}
   385  		w.WriteHeader(http.StatusCreated)
   386  		mustWrite(w, `{"data": {"id": "tweet-123"}}`)
   387  	})
   388  	cl := realTwitterClient{twitterAPI: &http.Client{Transport: localRoundTripper{mux}}}
   389  
   390  	tweetURL, err := cl.PostTweet("tweet-text", []byte("image-png-bytes"), "alt text")
   391  	if err != nil {
   392  		t.Fatal("PostTweet:", err)
   393  	}
   394  	if got, want := tweetURL, "https://twitter.com/username/status/tweet-123"; got != want {
   395  		t.Errorf("got tweetURL=%q, want %q", got, want)
   396  	}
   397  }
   398  
   399  // localRoundTripper is an http.RoundTripper that executes HTTP transactions
   400  // by using handler directly, instead of going over an HTTP connection.
   401  type localRoundTripper struct {
   402  	handler http.Handler
   403  }
   404  
   405  func (l localRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
   406  	w := httptest.NewRecorder()
   407  	l.handler.ServeHTTP(w, req)
   408  	return w.Result(), nil
   409  }
   410  
   411  func mustRead(r io.Reader) string {
   412  	b, err := io.ReadAll(r)
   413  	if err != nil {
   414  		panic(err)
   415  	}
   416  	return string(b)
   417  }
   418  
   419  func mustWrite(w io.Writer, s string) {
   420  	_, err := io.WriteString(w, s)
   421  	if err != nil {
   422  		panic(err)
   423  	}
   424  }