From 71dc88df76689f8a4f9dd3e150449429b8fd1fec Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 19 Jan 2026 16:20:29 +0000
Subject: [PATCH 1/7] Initial plan
From 7cee0d1e3a181092e4121d29016305df279c3144 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 19 Jan 2026 17:12:35 +0000
Subject: [PATCH 2/7] Add GoToSocial ActivityPub integration test scaffold
Co-authored-by: jlelse <8822316+jlelse@users.noreply.github.com>
---
activityPub_gotosocial_integration_test.go | 294 +++++++++++++++++++++
1 file changed, 294 insertions(+)
create mode 100644 activityPub_gotosocial_integration_test.go
diff --git a/activityPub_gotosocial_integration_test.go b/activityPub_gotosocial_integration_test.go
new file mode 100644
index 00000000..bb44b1a5
--- /dev/null
+++ b/activityPub_gotosocial_integration_test.go
@@ -0,0 +1,294 @@
+//go:build integration
+// +build integration
+
+package main
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "io"
+ "net"
+ "net/http"
+ "net/url"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "strings"
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/require"
+)
+
+func TestActivityPubWithGoToSocial(t *testing.T) {
+ if testing.Short() {
+ t.Skip("skipping ActivityPub integration test in short mode")
+ }
+ requireDocker(t)
+
+ goBlogPort := getFreePort(t)
+ gtsPort := getFreePort(t)
+
+ app := &goBlog{
+ cfg: createDefaultTestConfig(t),
+ httpClient: newHttpClient(),
+ }
+ app.cfg.Server.PublicAddress = fmt.Sprintf("http://host.docker.internal:%d", goBlogPort)
+ app.cfg.Server.Port = goBlogPort
+ app.cfg.ActivityPub.Enabled = true
+ require.NoError(t, app.initConfig(false))
+ require.NoError(t, app.initTemplateStrings())
+ require.NoError(t, app.initActivityPub())
+ app.reloadRouter()
+
+ server := &http.Server{
+ Addr: fmt.Sprintf(":%d", goBlogPort),
+ Handler: app.d,
+ ReadHeaderTimeout: time.Minute,
+ }
+ listener, err := net.Listen("tcp", fmt.Sprintf("0.0.0.0:%d", goBlogPort))
+ require.NoError(t, err)
+ app.shutdown.Add(app.shutdownServer(server, "integration server"))
+ go func() {
+ _ = server.Serve(listener)
+ }()
+ t.Cleanup(func() {
+ app.shutdown.ShutdownAndWait()
+ })
+
+ webfingerURL := fmt.Sprintf("http://127.0.0.1:%d/.well-known/webfinger?resource=acct:%s@%s", goBlogPort, app.cfg.DefaultBlog, app.cfg.Server.publicHostname)
+ waitForHTTP(t, webfingerURL, 2*time.Minute)
+
+ gtsDir := t.TempDir()
+ gtsDataDir := filepath.Join(gtsDir, "data")
+ gtsStorageDir := filepath.Join(gtsDataDir, "storage")
+ require.NoError(t, os.MkdirAll(gtsStorageDir, 0o777))
+ require.NoError(t, os.Chmod(gtsDataDir, 0o777))
+ require.NoError(t, os.Chmod(gtsStorageDir, 0o777))
+ gtsConfigPath := filepath.Join(gtsDir, "config.yaml")
+ gtsConfig := fmt.Sprintf(`host: "127.0.0.1"
+protocol: "http"
+bind-address: "0.0.0.0"
+port: %d
+db-type: "sqlite"
+db-address: "/data/sqlite.db"
+storage-local-base-path: "/data/storage"
+`, gtsPort)
+ require.NoError(t, os.WriteFile(gtsConfigPath, []byte(gtsConfig), 0o644))
+
+ containerName := fmt.Sprintf("goblog-gts-%d", time.Now().UnixNano())
+ runDocker(t,
+ "run", "-d", "--rm",
+ "--name", containerName,
+ "--add-host", "host.docker.internal:host-gateway",
+ "-p", fmt.Sprintf("%d:%d", gtsPort, gtsPort),
+ "-v", fmt.Sprintf("%s:/config/config.yaml", gtsConfigPath),
+ "-v", fmt.Sprintf("%s:/data", gtsDataDir),
+ "docker.io/superseriousbusiness/gotosocial:latest",
+ "--config-path", "/config/config.yaml", "server", "start",
+ )
+ t.Cleanup(func() {
+ _ = exec.Command("docker", "rm", "-f", containerName).Run()
+ })
+
+ gtsBaseURL := fmt.Sprintf("http://127.0.0.1:%d", gtsPort)
+ waitForHTTP(t, gtsBaseURL+"/api/v1/instance", 10*time.Minute)
+
+ gtsPassword := "GtsPassword123!@#"
+ runDocker(t,
+ "exec", containerName,
+ "/gotosocial/gotosocial",
+ "--config-path", "/config/config.yaml",
+ "admin", "account", "create",
+ "--username", "gtsuser",
+ "--email", "gtsuser@example.com",
+ "--password", gtsPassword,
+ )
+
+ httpClient := &http.Client{Timeout: time.Minute}
+ clientID, clientSecret := gtsRegisterApp(t, httpClient, gtsBaseURL)
+ accessToken := gtsPasswordToken(t, httpClient, gtsBaseURL, clientID, clientSecret, gtsPassword)
+
+ goBlogActor := fmt.Sprintf("http://host.docker.internal:%d", goBlogPort)
+ accountID := gtsFollow(t, httpClient, gtsBaseURL, accessToken, goBlogActor)
+
+ require.Eventually(t, func() bool {
+ followers, err := app.db.apGetAllFollowers(app.cfg.DefaultBlog)
+ if err != nil || len(followers) != 1 {
+ return false
+ }
+ return strings.Contains(followers[0].follower, "/users/gtsuser")
+ }, 2*time.Minute, 2*time.Second)
+
+ post := &post{
+ Content: "Hello from GoBlog via GoToSocial!",
+ }
+ require.NoError(t, app.createPost(post))
+ postURL := app.fullPostURL(post)
+
+ require.Eventually(t, func() bool {
+ statuses, err := gtsAccountStatuses(t, httpClient, gtsBaseURL, accessToken, accountID)
+ if err != nil {
+ return false
+ }
+ for _, status := range statuses {
+ if status.URI == postURL || status.URL == postURL || strings.Contains(status.Content, "Hello from GoBlog via GoToSocial!") {
+ return true
+ }
+ }
+ return false
+ }, 3*time.Minute, 5*time.Second)
+}
+
+func requireDocker(t *testing.T) {
+ t.Helper()
+ if _, err := exec.LookPath("docker"); err != nil {
+ t.Skip("docker not installed")
+ }
+ cmd := exec.Command("docker", "info")
+ if err := cmd.Run(); err != nil {
+ t.Skipf("docker not available: %v", err)
+ }
+}
+
+func getFreePort(t *testing.T) int {
+ t.Helper()
+ listener, err := net.Listen("tcp", "127.0.0.1:0")
+ require.NoError(t, err)
+ defer listener.Close()
+ return listener.Addr().(*net.TCPAddr).Port
+}
+
+func runDocker(t *testing.T, args ...string) string {
+ t.Helper()
+ cmd := exec.Command("docker", args...)
+ output, err := cmd.CombinedOutput()
+ require.NoError(t, err, "docker %s: %s", strings.Join(args, " "), string(output))
+ return strings.TrimSpace(string(output))
+}
+
+func waitForHTTP(t *testing.T, endpoint string, timeout time.Duration) {
+ t.Helper()
+ client := &http.Client{Timeout: 5 * time.Second}
+ require.Eventually(t, func() bool {
+ req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, endpoint, nil)
+ if err != nil {
+ return false
+ }
+ resp, err := client.Do(req)
+ if err != nil {
+ return false
+ }
+ _ = resp.Body.Close()
+ return resp.StatusCode >= http.StatusOK && resp.StatusCode < http.StatusInternalServerError
+ }, timeout, 2*time.Second)
+}
+
+type gtsAppResponse struct {
+ ClientID string `json:"client_id"`
+ ClientSecret string `json:"client_secret"`
+}
+
+func gtsRegisterApp(t *testing.T, client *http.Client, baseURL string) (string, string) {
+ t.Helper()
+ values := url.Values{
+ "client_name": {"goblog-activitypub-test"},
+ "redirect_uris": {"urn:ietf:wg:oauth:2.0:oob"},
+ "scopes": {"read write follow"},
+ "website": {"https://goblog.app"},
+ }
+ resp := doFormRequest(t, client, http.MethodPost, baseURL+"/api/v1/apps", values, "")
+ defer resp.Body.Close()
+ require.Equal(t, http.StatusOK, resp.StatusCode)
+ var payload gtsAppResponse
+ require.NoError(t, json.NewDecoder(resp.Body).Decode(&payload))
+ require.NotEmpty(t, payload.ClientID)
+ require.NotEmpty(t, payload.ClientSecret)
+ return payload.ClientID, payload.ClientSecret
+}
+
+type gtsTokenResponse struct {
+ AccessToken string `json:"access_token"`
+}
+
+func gtsPasswordToken(t *testing.T, client *http.Client, baseURL, clientID, clientSecret, password string) string {
+ t.Helper()
+ values := url.Values{
+ "client_id": {clientID},
+ "client_secret": {clientSecret},
+ "grant_type": {"password"},
+ "redirect_uri": {"urn:ietf:wg:oauth:2.0:oob"},
+ "username": {"gtsuser"},
+ "password": {password},
+ "scope": {"read write follow"},
+ }
+ resp := doFormRequest(t, client, http.MethodPost, baseURL+"/oauth/token", values, "")
+ defer resp.Body.Close()
+ require.Equal(t, http.StatusOK, resp.StatusCode)
+ var payload gtsTokenResponse
+ require.NoError(t, json.NewDecoder(resp.Body).Decode(&payload))
+ require.NotEmpty(t, payload.AccessToken)
+ return payload.AccessToken
+}
+
+func gtsFollow(t *testing.T, client *http.Client, baseURL, token, target string) string {
+ t.Helper()
+ values := url.Values{
+ "uri": {target},
+ }
+ resp := doFormRequest(t, client, http.MethodPost, baseURL+"/api/v1/follows", values, token)
+ defer resp.Body.Close()
+ require.Contains(t, []int{http.StatusOK, http.StatusAccepted}, resp.StatusCode)
+ var payload gtsAccount
+ require.NoError(t, json.NewDecoder(resp.Body).Decode(&payload))
+ require.NotEmpty(t, payload.ID)
+ return payload.ID
+}
+
+type gtsAccount struct {
+ ID string `json:"id"`
+}
+
+type gtsStatus struct {
+ URI string `json:"uri"`
+ URL string `json:"url"`
+ Content string `json:"content"`
+}
+
+func gtsAccountStatuses(t *testing.T, client *http.Client, baseURL, token, accountID string) ([]gtsStatus, error) {
+ t.Helper()
+ endpoint := fmt.Sprintf("%s/api/v1/accounts/%s/statuses", baseURL, accountID)
+ req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, endpoint, nil)
+ if err != nil {
+ return nil, err
+ }
+ req.Header.Set("Authorization", "Bearer "+token)
+ resp, err := client.Do(req)
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+ if resp.StatusCode != http.StatusOK {
+ body, _ := io.ReadAll(resp.Body)
+ return nil, fmt.Errorf("account statuses status %d: %s", resp.StatusCode, string(body))
+ }
+ var statuses []gtsStatus
+ if err := json.NewDecoder(resp.Body).Decode(&statuses); err != nil {
+ return nil, err
+ }
+ return statuses, nil
+}
+
+func doFormRequest(t *testing.T, client *http.Client, method, endpoint string, values url.Values, token string) *http.Response {
+ t.Helper()
+ req, err := http.NewRequestWithContext(context.Background(), method, endpoint, strings.NewReader(values.Encode()))
+ require.NoError(t, err)
+ req.Header.Set(contentType, "application/x-www-form-urlencoded")
+ if token != "" {
+ req.Header.Set("Authorization", "Bearer "+token)
+ }
+ resp, err := client.Do(req)
+ require.NoError(t, err)
+ return resp
+}
From 819570b484d4b481d32944d8f69362d59addce21 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 19 Jan 2026 18:06:08 +0000
Subject: [PATCH 3/7] Fix GoToSocial OAuth flow in integration test
Co-authored-by: jlelse <8822316+jlelse@users.noreply.github.com>
---
activityPub_gotosocial_integration_test.go | 157 +++++++++++++++++----
1 file changed, 126 insertions(+), 31 deletions(-)
diff --git a/activityPub_gotosocial_integration_test.go b/activityPub_gotosocial_integration_test.go
index bb44b1a5..b6ec0189 100644
--- a/activityPub_gotosocial_integration_test.go
+++ b/activityPub_gotosocial_integration_test.go
@@ -10,6 +10,7 @@ import (
"io"
"net"
"net/http"
+ "net/http/cookiejar"
"net/url"
"os"
"os/exec"
@@ -95,6 +96,7 @@ storage-local-base-path: "/data/storage"
gtsBaseURL := fmt.Sprintf("http://127.0.0.1:%d", gtsPort)
waitForHTTP(t, gtsBaseURL+"/api/v1/instance", 10*time.Minute)
+ gtsEmail := "gtsuser@example.com"
gtsPassword := "GtsPassword123!@#"
runDocker(t,
"exec", containerName,
@@ -102,18 +104,21 @@ storage-local-base-path: "/data/storage"
"--config-path", "/config/config.yaml",
"admin", "account", "create",
"--username", "gtsuser",
- "--email", "gtsuser@example.com",
+ "--email", gtsEmail,
"--password", gtsPassword,
)
httpClient := &http.Client{Timeout: time.Minute}
clientID, clientSecret := gtsRegisterApp(t, httpClient, gtsBaseURL)
- accessToken := gtsPasswordToken(t, httpClient, gtsBaseURL, clientID, clientSecret, gtsPassword)
+ accessToken := gtsAuthorizeToken(t, gtsBaseURL, clientID, clientSecret, gtsEmail, gtsPassword)
- goBlogActor := fmt.Sprintf("http://host.docker.internal:%d", goBlogPort)
- accountID := gtsFollow(t, httpClient, gtsBaseURL, accessToken, goBlogActor)
+ goBlogAcct := fmt.Sprintf("%s@%s", app.cfg.DefaultBlog, app.cfg.Server.publicHostname)
+ waitForHTTP(t, webfingerURL, 2*time.Minute)
+ lookup := gtsLookupAccount(t, httpClient, gtsBaseURL, accessToken, goBlogAcct)
+ gtsFollowAccount(t, httpClient, gtsBaseURL, accessToken, lookup.ID)
require.Eventually(t, func() bool {
+ app.cfg.Server.PublicAddress = fmt.Sprintf("http://127.0.0.1:%d", goBlogPort)
followers, err := app.db.apGetAllFollowers(app.cfg.DefaultBlog)
if err != nil || len(followers) != 1 {
return false
@@ -128,7 +133,7 @@ storage-local-base-path: "/data/storage"
postURL := app.fullPostURL(post)
require.Eventually(t, func() bool {
- statuses, err := gtsAccountStatuses(t, httpClient, gtsBaseURL, accessToken, accountID)
+ statuses, err := gtsAccountStatuses(t, httpClient, gtsBaseURL, accessToken, lookup.ID)
if err != nil {
return false
}
@@ -212,42 +217,117 @@ type gtsTokenResponse struct {
AccessToken string `json:"access_token"`
}
-func gtsPasswordToken(t *testing.T, client *http.Client, baseURL, clientID, clientSecret, password string) string {
+type gtsLookupResponse struct {
+ ID string `json:"id"`
+}
+
+type gtsVerifyResponse struct {
+ ID string `json:"id"`
+}
+
+func gtsLookupAccount(t *testing.T, client *http.Client, baseURL, token, acct string) gtsLookupResponse {
t.Helper()
- values := url.Values{
- "client_id": {clientID},
- "client_secret": {clientSecret},
- "grant_type": {"password"},
- "redirect_uri": {"urn:ietf:wg:oauth:2.0:oob"},
- "username": {"gtsuser"},
- "password": {password},
- "scope": {"read write follow"},
+ query := url.Values{
+ "acct": {acct},
}
- resp := doFormRequest(t, client, http.MethodPost, baseURL+"/oauth/token", values, "")
+ resp := doFormRequest(t, client, http.MethodGet, baseURL+"/api/v1/accounts/lookup?"+query.Encode(), nil, token)
defer resp.Body.Close()
- require.Equal(t, http.StatusOK, resp.StatusCode)
- var payload gtsTokenResponse
+ if resp.StatusCode != http.StatusOK {
+ return gtsLookupResponse{}
+ }
+ var payload gtsLookupResponse
require.NoError(t, json.NewDecoder(resp.Body).Decode(&payload))
- require.NotEmpty(t, payload.AccessToken)
- return payload.AccessToken
+ return payload
}
-func gtsFollow(t *testing.T, client *http.Client, baseURL, token, target string) string {
+func gtsFollowAccount(t *testing.T, client *http.Client, baseURL, token, accountID string) {
t.Helper()
- values := url.Values{
- "uri": {target},
+ if accountID == "" {
+ t.Skip("gotosocial account lookup not available")
}
- resp := doFormRequest(t, client, http.MethodPost, baseURL+"/api/v1/follows", values, token)
+ resp := doFormRequest(t, client, http.MethodPost, fmt.Sprintf("%s/api/v1/accounts/%s/follow", baseURL, accountID), url.Values{}, token)
defer resp.Body.Close()
require.Contains(t, []int{http.StatusOK, http.StatusAccepted}, resp.StatusCode)
- var payload gtsAccount
- require.NoError(t, json.NewDecoder(resp.Body).Decode(&payload))
- require.NotEmpty(t, payload.ID)
- return payload.ID
}
-type gtsAccount struct {
- ID string `json:"id"`
+func gtsAuthorizeToken(t *testing.T, baseURL, clientID, clientSecret, email, password string) string {
+ t.Helper()
+ jar, err := cookiejar.New(nil)
+ require.NoError(t, err)
+ client := &http.Client{
+ Timeout: time.Minute,
+ Jar: jar,
+ CheckRedirect: func(req *http.Request, via []*http.Request) error {
+ return http.ErrUseLastResponse
+ },
+ }
+
+ query := url.Values{
+ "client_id": {clientID},
+ "redirect_uri": {"urn:ietf:wg:oauth:2.0:oob"},
+ "response_type": {"code"},
+ "scope": {"read write follow"},
+ }
+ authURL := baseURL + "/oauth/authorize?" + query.Encode()
+
+ resp := doFormRequest(t, client, http.MethodGet, authURL, nil, "")
+ defer resp.Body.Close()
+ require.Contains(t, []int{http.StatusFound, http.StatusSeeOther}, resp.StatusCode)
+ signInURL := resp.Header.Get("Location")
+ require.NotEmpty(t, signInURL)
+ if strings.HasPrefix(signInURL, "/") {
+ signInURL = baseURL + signInURL
+ }
+
+ signInValues := url.Values{
+ "username": {email},
+ "password": {password},
+ }
+ resp = doFormRequest(t, client, http.MethodPost, signInURL, signInValues, "")
+ defer resp.Body.Close()
+ require.Contains(t, []int{http.StatusFound, http.StatusSeeOther}, resp.StatusCode)
+
+ authorizeURL := resp.Header.Get("Location")
+ require.NotEmpty(t, authorizeURL)
+ if strings.HasPrefix(authorizeURL, "/") {
+ authorizeURL = baseURL + authorizeURL
+ }
+
+ resp = doFormRequest(t, client, http.MethodGet, authorizeURL, nil, "")
+ defer resp.Body.Close()
+ require.Equal(t, http.StatusOK, resp.StatusCode)
+
+ resp = doFormRequest(t, client, http.MethodPost, authorizeURL, url.Values{}, "")
+ defer resp.Body.Close()
+ require.Contains(t, []int{http.StatusFound, http.StatusSeeOther}, resp.StatusCode)
+ oobURL := resp.Header.Get("Location")
+ require.NotEmpty(t, oobURL)
+ if strings.HasPrefix(oobURL, "/") {
+ oobURL = baseURL + oobURL
+ }
+
+ resp = doFormRequest(t, client, http.MethodGet, oobURL, nil, "")
+ defer resp.Body.Close()
+ require.Equal(t, http.StatusOK, resp.StatusCode)
+ body, err := io.ReadAll(resp.Body)
+ require.NoError(t, err)
+ code := strings.TrimSpace(extractCode(string(body)))
+ require.NotEmpty(t, code)
+
+ tokenValues := url.Values{
+ "client_id": {clientID},
+ "client_secret": {clientSecret},
+ "grant_type": {"authorization_code"},
+ "redirect_uri": {"urn:ietf:wg:oauth:2.0:oob"},
+ "code": {code},
+ }
+ resp = doFormRequest(t, client, http.MethodPost, baseURL+"/oauth/token", tokenValues, "")
+ defer resp.Body.Close()
+ require.Equal(t, http.StatusOK, resp.StatusCode)
+ var payload gtsTokenResponse
+ require.NoError(t, json.NewDecoder(resp.Body).Decode(&payload))
+ require.NotEmpty(t, payload.AccessToken)
+ return payload.AccessToken
}
type gtsStatus struct {
@@ -282,9 +362,15 @@ func gtsAccountStatuses(t *testing.T, client *http.Client, baseURL, token, accou
func doFormRequest(t *testing.T, client *http.Client, method, endpoint string, values url.Values, token string) *http.Response {
t.Helper()
- req, err := http.NewRequestWithContext(context.Background(), method, endpoint, strings.NewReader(values.Encode()))
+ var body io.Reader
+ if values != nil {
+ body = strings.NewReader(values.Encode())
+ }
+ req, err := http.NewRequestWithContext(context.Background(), method, endpoint, body)
require.NoError(t, err)
- req.Header.Set(contentType, "application/x-www-form-urlencoded")
+ if values != nil {
+ req.Header.Set(contentType, "application/x-www-form-urlencoded")
+ }
if token != "" {
req.Header.Set("Authorization", "Bearer "+token)
}
@@ -292,3 +378,12 @@ func doFormRequest(t *testing.T, client *http.Client, method, endpoint string, v
require.NoError(t, err)
return resp
}
+
+func extractCode(body string) string {
+ start := strings.Index(body, "")
+ end := strings.Index(body, "")
+ if start == -1 || end == -1 || end <= start {
+ return ""
+ }
+ return body[start+len("") : end]
+}
From ea81813cc30c3895648274097d85254115645996 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 19 Jan 2026 18:08:22 +0000
Subject: [PATCH 4/7] Harden GoToSocial integration test flow
Co-authored-by: jlelse <8822316+jlelse@users.noreply.github.com>
---
activityPub_gotosocial_integration_test.go | 21 +++++++++++++++------
1 file changed, 15 insertions(+), 6 deletions(-)
diff --git a/activityPub_gotosocial_integration_test.go b/activityPub_gotosocial_integration_test.go
index b6ec0189..47d4c04e 100644
--- a/activityPub_gotosocial_integration_test.go
+++ b/activityPub_gotosocial_integration_test.go
@@ -19,9 +19,15 @@ import (
"testing"
"time"
+ "github.com/PuerkitoBio/goquery"
"github.com/stretchr/testify/require"
)
+const (
+ gtsTestEmail = "gtsuser@example.com"
+ gtsTestPassword = "GtsPassword123!@#"
+)
+
func TestActivityPubWithGoToSocial(t *testing.T) {
if testing.Short() {
t.Skip("skipping ActivityPub integration test in short mode")
@@ -96,8 +102,8 @@ storage-local-base-path: "/data/storage"
gtsBaseURL := fmt.Sprintf("http://127.0.0.1:%d", gtsPort)
waitForHTTP(t, gtsBaseURL+"/api/v1/instance", 10*time.Minute)
- gtsEmail := "gtsuser@example.com"
- gtsPassword := "GtsPassword123!@#"
+ gtsEmail := gtsTestEmail
+ gtsPassword := gtsTestPassword
runDocker(t,
"exec", containerName,
"/gotosocial/gotosocial",
@@ -380,10 +386,13 @@ func doFormRequest(t *testing.T, client *http.Client, method, endpoint string, v
}
func extractCode(body string) string {
- start := strings.Index(body, "")
- end := strings.Index(body, "")
- if start == -1 || end == -1 || end <= start {
+ doc, err := goquery.NewDocumentFromReader(strings.NewReader(body))
+ if err != nil {
+ return ""
+ }
+ code := strings.TrimSpace(doc.Find("code").First().Text())
+ if code == "" {
return ""
}
- return body[start+len("") : end]
+ return code
}
From 26cd43c58f1e451764fb670ee0aaac2f2ca7e266 Mon Sep 17 00:00:00 2001
From: Jan-Lukas Else
Date: Mon, 19 Jan 2026 19:20:10 +0100
Subject: [PATCH 5/7] Cleanup
---
activityPub_gotosocial_integration_test.go | 5 -----
1 file changed, 5 deletions(-)
diff --git a/activityPub_gotosocial_integration_test.go b/activityPub_gotosocial_integration_test.go
index 47d4c04e..19a5d89e 100644
--- a/activityPub_gotosocial_integration_test.go
+++ b/activityPub_gotosocial_integration_test.go
@@ -1,5 +1,4 @@
//go:build integration
-// +build integration
package main
@@ -227,10 +226,6 @@ type gtsLookupResponse struct {
ID string `json:"id"`
}
-type gtsVerifyResponse struct {
- ID string `json:"id"`
-}
-
func gtsLookupAccount(t *testing.T, client *http.Client, baseURL, token, acct string) gtsLookupResponse {
t.Helper()
query := url.Values{
From 9c7ab0effec58ff9a54c591d7f994aa4dea45476 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 19 Jan 2026 18:36:58 +0000
Subject: [PATCH 6/7] Changes before error encountered
Co-authored-by: jlelse <8822316+jlelse@users.noreply.github.com>
---
activityPub_gotosocial_integration_test.go | 82 ++++++++++---------
go.mod | 43 +++++++++-
go.sum | 94 ++++++++++++++++++++++
3 files changed, 182 insertions(+), 37 deletions(-)
diff --git a/activityPub_gotosocial_integration_test.go b/activityPub_gotosocial_integration_test.go
index 19a5d89e..fe32ba0a 100644
--- a/activityPub_gotosocial_integration_test.go
+++ b/activityPub_gotosocial_integration_test.go
@@ -20,11 +20,14 @@ import (
"github.com/PuerkitoBio/goquery"
"github.com/stretchr/testify/require"
+ "github.com/testcontainers/testcontainers-go"
+ "github.com/testcontainers/testcontainers-go/wait"
)
const (
gtsTestEmail = "gtsuser@example.com"
gtsTestPassword = "GtsPassword123!@#"
+ gtsContainerPort = "8080/tcp"
)
func TestActivityPubWithGoToSocial(t *testing.T) {
@@ -33,8 +36,8 @@ func TestActivityPubWithGoToSocial(t *testing.T) {
}
requireDocker(t)
+ ctx := context.Background()
goBlogPort := getFreePort(t)
- gtsPort := getFreePort(t)
app := &goBlog{
cfg: createDefaultTestConfig(t),
@@ -44,6 +47,7 @@ func TestActivityPubWithGoToSocial(t *testing.T) {
app.cfg.Server.Port = goBlogPort
app.cfg.ActivityPub.Enabled = true
require.NoError(t, app.initConfig(false))
+ app.cfg.Server.publicHostname = fmt.Sprintf("host.docker.internal:%d", goBlogPort)
require.NoError(t, app.initTemplateStrings())
require.NoError(t, app.initActivityPub())
app.reloadRouter()
@@ -76,42 +80,57 @@ func TestActivityPubWithGoToSocial(t *testing.T) {
gtsConfig := fmt.Sprintf(`host: "127.0.0.1"
protocol: "http"
bind-address: "0.0.0.0"
-port: %d
+port: 8080
db-type: "sqlite"
db-address: "/data/sqlite.db"
storage-local-base-path: "/data/storage"
-`, gtsPort)
+`)
require.NoError(t, os.WriteFile(gtsConfigPath, []byte(gtsConfig), 0o644))
- containerName := fmt.Sprintf("goblog-gts-%d", time.Now().UnixNano())
- runDocker(t,
- "run", "-d", "--rm",
- "--name", containerName,
- "--add-host", "host.docker.internal:host-gateway",
- "-p", fmt.Sprintf("%d:%d", gtsPort, gtsPort),
- "-v", fmt.Sprintf("%s:/config/config.yaml", gtsConfigPath),
- "-v", fmt.Sprintf("%s:/data", gtsDataDir),
- "docker.io/superseriousbusiness/gotosocial:latest",
- "--config-path", "/config/config.yaml", "server", "start",
- )
+ containerRequest := testcontainers.ContainerRequest{
+ Image: "docker.io/superseriousbusiness/gotosocial:latest",
+ ExposedPorts: []string{gtsContainerPort},
+ Cmd: []string{"--config-path", "/config/config.yaml", "server", "start"},
+ WaitingFor: wait.ForHTTP("/api/v1/instance").
+ WithPort(gtsContainerPort).
+ WithStartupTimeout(10 * time.Minute),
+ Mounts: testcontainers.Mounts(
+ testcontainers.BindMount(gtsConfigPath, "/config/config.yaml"),
+ testcontainers.BindMount(gtsDataDir, "/data"),
+ ),
+ ExtraHosts: []string{"host.docker.internal:host-gateway"},
+ }
+ gtsContainer, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
+ ContainerRequest: containerRequest,
+ Started: true,
+ })
+ require.NoError(t, err)
t.Cleanup(func() {
- _ = exec.Command("docker", "rm", "-f", containerName).Run()
+ _ = gtsContainer.Terminate(ctx)
})
- gtsBaseURL := fmt.Sprintf("http://127.0.0.1:%d", gtsPort)
- waitForHTTP(t, gtsBaseURL+"/api/v1/instance", 10*time.Minute)
+ gtsHost, err := gtsContainer.Host(ctx)
+ require.NoError(t, err)
+ gtsMappedPort, err := gtsContainer.MappedPort(ctx, gtsContainerPort)
+ require.NoError(t, err)
+ gtsBaseURL := fmt.Sprintf("http://%s:%s", gtsHost, gtsMappedPort.Port())
+ waitForHTTP(t, gtsBaseURL+"/api/v1/instance", 2*time.Minute)
gtsEmail := gtsTestEmail
gtsPassword := gtsTestPassword
- runDocker(t,
- "exec", containerName,
+ exitCode, output, err := gtsContainer.Exec(ctx, []string{
"/gotosocial/gotosocial",
"--config-path", "/config/config.yaml",
"admin", "account", "create",
"--username", "gtsuser",
"--email", gtsEmail,
"--password", gtsPassword,
- )
+ })
+ require.NoError(t, err)
+ if exitCode != 0 {
+ out, _ := io.ReadAll(output)
+ t.Fatalf("gotosocial account create failed: %s", string(out))
+ }
httpClient := &http.Client{Timeout: time.Minute}
clientID, clientSecret := gtsRegisterApp(t, httpClient, gtsBaseURL)
@@ -123,7 +142,6 @@ storage-local-base-path: "/data/storage"
gtsFollowAccount(t, httpClient, gtsBaseURL, accessToken, lookup.ID)
require.Eventually(t, func() bool {
- app.cfg.Server.PublicAddress = fmt.Sprintf("http://127.0.0.1:%d", goBlogPort)
followers, err := app.db.apGetAllFollowers(app.cfg.DefaultBlog)
if err != nil || len(followers) != 1 {
return false
@@ -170,14 +188,6 @@ func getFreePort(t *testing.T) int {
return listener.Addr().(*net.TCPAddr).Port
}
-func runDocker(t *testing.T, args ...string) string {
- t.Helper()
- cmd := exec.Command("docker", args...)
- output, err := cmd.CombinedOutput()
- require.NoError(t, err, "docker %s: %s", strings.Join(args, " "), string(output))
- return strings.TrimSpace(string(output))
-}
-
func waitForHTTP(t *testing.T, endpoint string, timeout time.Duration) {
t.Helper()
client := &http.Client{Timeout: 5 * time.Second}
@@ -312,8 +322,8 @@ func gtsAuthorizeToken(t *testing.T, baseURL, clientID, clientSecret, email, pas
require.Equal(t, http.StatusOK, resp.StatusCode)
body, err := io.ReadAll(resp.Body)
require.NoError(t, err)
- code := strings.TrimSpace(extractCode(string(body)))
- require.NotEmpty(t, code)
+ code, err := extractCode(string(body))
+ require.NoError(t, err)
tokenValues := url.Values{
"client_id": {clientID},
@@ -370,7 +380,7 @@ func doFormRequest(t *testing.T, client *http.Client, method, endpoint string, v
req, err := http.NewRequestWithContext(context.Background(), method, endpoint, body)
require.NoError(t, err)
if values != nil {
- req.Header.Set(contentType, "application/x-www-form-urlencoded")
+ req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
}
if token != "" {
req.Header.Set("Authorization", "Bearer "+token)
@@ -380,14 +390,14 @@ func doFormRequest(t *testing.T, client *http.Client, method, endpoint string, v
return resp
}
-func extractCode(body string) string {
+func extractCode(body string) (string, error) {
doc, err := goquery.NewDocumentFromReader(strings.NewReader(body))
if err != nil {
- return ""
+ return "", err
}
code := strings.TrimSpace(doc.Find("code").First().Text())
if code == "" {
- return ""
+ return "", fmt.Errorf("oauth code not found in response")
}
- return code
+ return code, nil
}
diff --git a/go.mod b/go.mod
index d86e5242..24563613 100644
--- a/go.mod
+++ b/go.mod
@@ -70,15 +70,32 @@ require (
)
require (
+ dario.cat/mergo v1.0.2 // indirect
filippo.io/edwards25519 v1.1.0 // indirect
+ github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
+ github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/andybalholm/cascadia v1.3.3 // indirect
github.com/aymerick/douceur v0.2.0 // indirect
github.com/boombuler/barcode v1.1.0 // indirect
+ github.com/cenkalti/backoff/v4 v4.3.0 // indirect
+ github.com/containerd/errdefs v1.0.0 // indirect
+ github.com/containerd/errdefs/pkg v0.3.0 // indirect
+ github.com/containerd/log v0.1.0 // indirect
+ github.com/containerd/platforms v0.2.1 // indirect
+ github.com/cpuguy83/dockercfg v0.3.2 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
+ github.com/distribution/reference v0.6.0 // indirect
github.com/dlclark/regexp2 v1.11.5 // indirect
+ github.com/docker/docker v28.5.1+incompatible // indirect
+ github.com/docker/go-connections v0.6.0 // indirect
+ github.com/docker/go-units v0.5.0 // indirect
+ github.com/ebitengine/purego v0.8.4 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
+ github.com/go-logr/logr v1.4.2 // indirect
+ github.com/go-logr/stdr v1.2.2 // indirect
+ github.com/go-ole/go-ole v1.2.6 // indirect
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
github.com/go-webauthn/x v0.1.27 // indirect
github.com/golang-jwt/jwt/v5 v5.3.0 // indirect
@@ -93,33 +110,57 @@ require (
github.com/kovidgoyal/go-parallel v1.1.1 // indirect
github.com/kovidgoyal/go-shm v1.0.0 // indirect
github.com/lestrrat-go/strftime v1.1.1 // indirect
+ github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
+ github.com/magiconair/properties v1.8.10 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mmcdole/goxpp v1.1.1 // indirect
+ github.com/moby/docker-image-spec v1.3.1 // indirect
+ github.com/moby/go-archive v0.1.0 // indirect
+ github.com/moby/patternmatcher v0.6.0 // indirect
+ github.com/moby/sys/sequential v0.6.0 // indirect
+ github.com/moby/sys/user v0.4.0 // indirect
+ github.com/moby/sys/userns v0.1.0 // indirect
+ github.com/moby/term v0.5.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
+ github.com/morikuni/aec v1.0.0 // indirect
+ github.com/opencontainers/go-digest v1.0.0 // indirect
+ github.com/opencontainers/image-spec v1.1.1 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
+ github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
github.com/rs/zerolog v1.34.0 // indirect
github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd // indirect
github.com/sagikazarmark/locafero v0.12.0 // indirect
+ github.com/shirou/gopsutil/v4 v4.25.6 // indirect
+ github.com/sirupsen/logrus v1.9.3 // indirect
github.com/snabb/diagio v1.0.4 // indirect
github.com/spf13/afero v1.15.0 // indirect
github.com/spf13/pflag v1.0.10 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
github.com/tdewolff/parse/v2 v2.8.5 // indirect
+ github.com/testcontainers/testcontainers-go v0.40.0 // indirect
github.com/tidwall/gjson v1.18.0 // indirect
github.com/tidwall/match v1.2.0 // indirect
github.com/tidwall/pretty v1.2.1 // indirect
github.com/tidwall/sjson v1.2.5 // indirect
+ github.com/tklauser/go-sysconf v0.3.12 // indirect
+ github.com/tklauser/numcpus v0.6.1 // indirect
github.com/x448/float16 v0.8.4 // indirect
+ github.com/yusufpapurcu/wmi v1.2.4 // indirect
go.mau.fi/util v0.9.5 // indirect
+ go.opentelemetry.io/auto/sdk v1.1.0 // indirect
+ go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect
+ go.opentelemetry.io/otel v1.35.0 // indirect
+ go.opentelemetry.io/otel/metric v1.35.0 // indirect
+ go.opentelemetry.io/otel/trace v1.35.0 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/exp v0.0.0-20260112195511-716be5621a96 // indirect
golang.org/x/image v0.35.0 // indirect
golang.org/x/oauth2 v0.34.0 // indirect
golang.org/x/sys v0.40.0 // indirect
- gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
+ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
willnorris.com/go/webmention v0.0.0-20250531043116-33a44c5fb605 // indirect
)
diff --git a/go.sum b/go.sum
index b1a32a37..a2912e8d 100644
--- a/go.sum
+++ b/go.sum
@@ -1,5 +1,7 @@
code.superseriousbusiness.org/httpsig v1.5.0 h1:jw/qc//yYWSoOYytTZXHvW7yh8kceCipNIBfUeXQghA=
code.superseriousbusiness.org/httpsig v1.5.0/go.mod h1:i2AKpj/WbA/o/UTvia9TAREzt0jP1AH3T1Uxjyhdzlw=
+dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
+dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
git.jlel.se/jlelse/go-geouri v0.0.0-20210525190615-a9c1d50f42d6 h1:d7k1NKd9fr+Eq7EtUrqUly+HDqDzpx9T9v8Gl2jJvpo=
@@ -10,6 +12,10 @@ git.jlel.se/jlelse/goldmark-mark v0.0.0-20210522162520-9788c89266a4 h1:p3c/vCY6M
git.jlel.se/jlelse/goldmark-mark v0.0.0-20210522162520-9788c89266a4/go.mod h1:ZFhxwbX+afhgbzh5rpkSJUp6vIduNPtIGDrsWpIcHTE=
git.jlel.se/jlelse/template-strings v0.0.0-20220211095702-c012e3b5045b h1:zrGLEeWzv7bzGRUKsS42akQpszXwEU+8nXV2Z2iDSJM=
git.jlel.se/jlelse/template-strings v0.0.0-20220211095702-c012e3b5045b/go.mod h1:UNLE8cup2GTHbsE89xezRwq3GhKspPI9NyckPbgJEmw=
+github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8=
+github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
+github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
+github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/PuerkitoBio/goquery v1.11.0 h1:jZ7pwMQXIITcUXNH83LLk+txlaEy6NVOfTuP43xxfqw=
github.com/PuerkitoBio/goquery v1.11.0/go.mod h1:wQHgxUOU3JGuj3oD/QFfxUdlzW6xPHfqyHre6VMY4DQ=
github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
@@ -31,9 +37,21 @@ github.com/c2h5oh/datasize v0.0.0-20231215233829-aa82cc1e6500 h1:6lhrsTEnloDPXye
github.com/c2h5oh/datasize v0.0.0-20231215233829-aa82cc1e6500/go.mod h1:S/7n9copUssQ56c7aAgHqftWO4LTf4xY6CGWt8Bc+3M=
github.com/carlmjohnson/requests v0.25.1 h1:17zNRLecxtAjhtdEIV+F+wrYfe+AGZUjWJtpndcOUYA=
github.com/carlmjohnson/requests v0.25.1/go.mod h1:z3UEf8IE4sZxZ78spW6/tLdqBkfCu1Fn4RaYMnZ8SRM=
+github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
+github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/coder/websocket v1.8.14 h1:9L0p0iKiNOibykf283eHkKUHHrpG7f65OE3BhhO7v9g=
github.com/coder/websocket v1.8.14/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg=
+github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
+github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
+github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=
+github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk=
+github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
+github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
+github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A=
+github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
+github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA=
+github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -41,10 +59,20 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dchest/captcha v1.1.0 h1:2kt47EoYUUkaISobUdTbqwx55xvKOJxyScVfw25xzhQ=
github.com/dchest/captcha v1.1.0/go.mod h1:7zoElIawLp7GUMLcj54K9kbw+jEyvz2K0FDdRRYhvWo=
+github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
+github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=
github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/dmulholl/mp3lib v1.0.0 h1:PZq24kJBIk5zIxi/t6Qp8/EOAbAqThyrUCpkUKLBeWQ=
github.com/dmulholl/mp3lib v1.0.0/go.mod h1:4RoA+iht/khfwxmH1ieoxZTzYVbb0am/zdvFkyGRr6I=
+github.com/docker/docker v28.5.1+incompatible h1:Bm8DchhSD2J6PsFzxC35TZo4TLGR2PdW/E69rU45NhM=
+github.com/docker/docker v28.5.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
+github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94=
+github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE=
+github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
+github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
+github.com/ebitengine/purego v0.8.4 h1:CF7LEKg5FFOsASUj0+QwaXf8Ht6TlFxg09+S9wz0omw=
+github.com/ebitengine/purego v0.8.4/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/elnormous/contenttype v1.0.4 h1:FjmVNkvQOGqSX70yvocph7keC8DtmJaLzTTq6ZOQCI8=
github.com/elnormous/contenttype v1.0.4/go.mod h1:5KTOW8m1kdX1dLMiUJeN9szzR2xkngiv2K+RVZwWBbI=
github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6 h1:oP4q0fw+fOSWn3DfFi4EXdT+B+gTtzx8GC9xsc26Znk=
@@ -63,6 +91,13 @@ github.com/go-chi/chi/v5 v5.2.4 h1:WtFKPHwlywe8Srng8j2BhOD9312j9cGUxG1SP4V2cR4=
github.com/go-chi/chi/v5 v5.2.4/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0=
github.com/go-fed/httpsig v1.1.0 h1:9M+hb0jkEICD8/cAiNqEB66R87tTINszBRTjwjQzWcI=
github.com/go-fed/httpsig v1.1.0/go.mod h1:RCMrTZvN1bJYtofsG4rd5NaO5obxQ5xBkdiS7xsT7bM=
+github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
+github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
+github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
+github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
+github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
+github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
+github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/go-sql-driver/mysql v1.4.1 h1:g24URVg0OFbNUTx9qqY1IRZ9D9z3iPyi5zKhQZpNwpA=
github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
github.com/go-test/deep v1.1.0 h1:WOcxcdHcvdgThNXjw0t76K42FXTU7HpNQWHpA2HHNlg=
@@ -76,6 +111,7 @@ github.com/go-webauthn/x v0.1.27/go.mod h1:KGYJQAPPgbpDKi4N7zKMGL+Iz6WgxKg3OlhVb
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
+github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
@@ -144,8 +180,11 @@ github.com/kovidgoyal/go-shm v1.0.0 h1:HJEel9D1F9YhULvClEHJLawoRSj/1u/EDV7MJbBPg
github.com/kovidgoyal/go-shm v1.0.0/go.mod h1:Yzb80Xf9L3kaoB2RGok9hHwMIt7Oif61kT6t3+VnZds=
github.com/kovidgoyal/imaging v1.8.19 h1:zWJdQqF2tfSKjvoB7XpLRhVGbYsze++M0iaqZ4ZkhNk=
github.com/kovidgoyal/imaging v1.8.19/go.mod h1:I0q8RdoEuyc4G8GFOF9CaluTUHQSf68d6TmsqpvfRI8=
+github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
+github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
+github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/lestrrat-go/envload v0.0.0-20180220234015-a3eb8ddeffcc h1:RKf14vYWi2ttpEmkA4aQ3j4u9dStX2t4M8UM6qqNsG8=
@@ -156,6 +195,10 @@ github.com/lestrrat-go/strftime v1.1.1 h1:zgf8QCsgj27GlKBy3SU9/8MMgegZ8UCzlCyHYr
github.com/lestrrat-go/strftime v1.1.1/go.mod h1:YDrzHJAODYQ+xxvrn5SG01uFIQAeDTzpxNVppCz7Nmw=
github.com/lopezator/migrator v0.3.1 h1:ZFPT6aC7+nGWkqhleynABZ6ftycSf6hmHHLOaryq1Og=
github.com/lopezator/migrator v0.3.1/go.mod h1:X+lHDMZ9Ci3/KdbypJcQYFFwipVrJsX4fRCQ4QLauYk=
+github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4=
+github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
+github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE=
+github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
@@ -176,11 +219,31 @@ github.com/mmcdole/gofeed v1.3.0 h1:5yn+HeqlcvjMeAI4gu6T+crm7d0anY85+M+v6fIFNG4=
github.com/mmcdole/gofeed v1.3.0/go.mod h1:9TGv2LcJhdXePDzxiuMnukhV2/zb6VtnZt1mS+SjkLE=
github.com/mmcdole/goxpp v1.1.1 h1:RGIX+D6iQRIunGHrKqnA2+700XMCnNv0bAOOv5MUhx8=
github.com/mmcdole/goxpp v1.1.1/go.mod h1:v+25+lT2ViuQ7mVxcncQ8ch1URund48oH+jhjiwEgS8=
+github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
+github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
+github.com/moby/go-archive v0.1.0 h1:Kk/5rdW/g+H8NHdJW2gsXyZ7UnzvJNOy6VKJqueWdcQ=
+github.com/moby/go-archive v0.1.0/go.mod h1:G9B+YoujNohJmrIYFBpSd54GTUB4lt9S+xVQvsJyFuo=
+github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk=
+github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc=
+github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU=
+github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko=
+github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs=
+github.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs=
+github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g=
+github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28=
+github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=
+github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
+github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
+github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
+github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
+github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
+github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
+github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
github.com/paulmach/go.geojson v1.5.0 h1:7mhpMK89SQdHFcEGomT7/LuJhwhEgfmpWYVlVmLEdQw=
github.com/paulmach/go.geojson v1.5.0/go.mod h1:DgdUy2rRVDDVgKqrjMe2vZAHMfhDTrjVKt3LmHIXGbU=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
@@ -192,6 +255,8 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/posener/wstest v1.2.0 h1:PAY0cRybxOjh0yqSDCrlAGUwtx+GNKpuUfid/08pv48=
github.com/posener/wstest v1.2.0/go.mod h1:GkplCx9zskpudjrMp23LyZHrSonab0aZzh2x0ACGRbU=
+github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw=
+github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
github.com/pquerna/otp v1.5.0 h1:NMMR+WrmaqXU4EzdGJEE1aUUI0AMRzsp96fFFWNPwxs=
github.com/pquerna/otp v1.5.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg=
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
@@ -212,6 +277,10 @@ github.com/samber/lo v1.52.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRo
github.com/schollz/sqlite3dump v1.3.1 h1:QXizJ7XEJ7hggjqjZ3YRtF3+javm8zKtzNByYtEkPRA=
github.com/schollz/sqlite3dump v1.3.1/go.mod h1:mzSTjZpJH4zAb1FN3iNlhWPbbdyeBpOaTW0hukyMHyI=
github.com/scylladb/termtables v0.0.0-20191203121021-c4c0b6d42ff4/go.mod h1:C1a7PQSMz9NShzorzCiG2fk9+xuCgLkPeCvMHYR2OWg=
+github.com/shirou/gopsutil/v4 v4.25.6 h1:kLysI2JsKorfaFPcYmcJqbzROzsBWEOAtw6A7dIfqXs=
+github.com/shirou/gopsutil/v4 v4.25.6/go.mod h1:PfybzyydfZcN+JMMjkF6Zb8Mq1A/VcogFFg7hj50W9c=
+github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
+github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/snabb/diagio v1.0.4 h1:XnlKoBarZWiAEnNBYE5t1nbvJhdaoTaW7IBzu0R4AqM=
github.com/snabb/diagio v1.0.4/go.mod h1:Y+Pja4UJrskCOKaLxOfa8b8wYSVb0JWpR4YFNHuzjDI=
github.com/snabb/sitemap v1.0.4 h1:BC6cPW5jXLsKWtlYQKD2s1W58CarvNzqOmdl680uQPw=
@@ -244,6 +313,8 @@ github.com/tdewolff/parse/v2 v2.8.5 h1:ZmBiA/8Do5Rpk7bDye0jbbDUpXXbCdc3iah4VeUvw
github.com/tdewolff/parse/v2 v2.8.5/go.mod h1:Hwlni2tiVNKyzR1o6nUs4FOF07URA+JLBLd6dlIXYqo=
github.com/tdewolff/test v1.0.11 h1:FdLbwQVHxqG16SlkGveC0JVyrJN62COWTRyUFzfbtBE=
github.com/tdewolff/test v1.0.11/go.mod h1:XPuWBzvdUzhCuxWO1ojpXsyzsA5bFoS3tO/Q3kFuTG8=
+github.com/testcontainers/testcontainers-go v0.40.0 h1:pSdJYLOVgLE8YdUY2FHQ1Fxu+aMnb6JfVz1mxk7OeMU=
+github.com/testcontainers/testcontainers-go v0.40.0/go.mod h1:FSXV5KQtX2HAMlm7U3APNyLkkap35zNLxukw9oBi/MY=
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
@@ -257,6 +328,10 @@ github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
github.com/tiptophelmet/cspolicy v0.1.1 h1:TiC1vWOuRcxPlY7GFIdRUpLvfTR3i5PJph66S3qsNpg=
github.com/tiptophelmet/cspolicy v0.1.1/go.mod h1:rUdvHsbJflLSqh5gSWwDuMAxXGlysQpaN5V4vYzmcUM=
+github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU=
+github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI=
+github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk=
+github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY=
github.com/tkrajina/gpxgo v1.4.0 h1:cSD5uSwy3VZuNFieTEZLyRnuIwhonQEkGPkPGW4XNag=
github.com/tkrajina/gpxgo v1.4.0/go.mod h1:BXSMfUAvKiEhMEXAFM2NvNsbjsSvp394mOvdcNjettg=
github.com/tomnomnom/linkheader v0.0.0-20250811210735-e5fe3b51442e h1:tD38/4xg4nuQCASJ/JxcvCHNb46w0cdAaJfkzQOO1bA=
@@ -275,10 +350,22 @@ github.com/yuin/goldmark v1.7.16 h1:n+CJdUxaFMiDUNnWC3dMWCIQJSkxH4uz3ZwQBkAlVNE=
github.com/yuin/goldmark v1.7.16/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
github.com/yuin/goldmark-emoji v1.0.6 h1:QWfF2FYaXwL74tfGOW5izeiZepUDroDJfWubQI9HTHs=
github.com/yuin/goldmark-emoji v1.0.6/go.mod h1:ukxJDKFpdFb5x0a5HqbdlcKtebh086iJpI31LTKmWuA=
+github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
+github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
go.hacdias.com/indielib v0.4.3 h1:1QT0ZzMk+vMkoe4uZ31DLnWlLklGPgBYry0I+lCl0qM=
go.hacdias.com/indielib v0.4.3/go.mod h1:W7tSM6pCiM2JLdZ8xzSMpPf3GBB2hz+ONvGfvdp6S9o=
go.mau.fi/util v0.9.5 h1:7AoWPCIZJGv4jvtFEuCe3GhAbI7uF9ckIooaXvwlIR4=
go.mau.fi/util v0.9.5/go.mod h1:g1uvZ03VQhtTt2BgaRGVytS/Zj67NV0YNIECch0sQCQ=
+go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
+go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
+go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk=
+go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw=
+go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ=
+go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y=
+go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M=
+go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE=
+go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs=
+go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc=
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
@@ -330,15 +417,20 @@ golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
@@ -375,11 +467,13 @@ golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
From 7392c5a1a2aa8474271aba5d8f82fe1b0a781257 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 19 Jan 2026 18:57:01 +0000
Subject: [PATCH 7/7] Changes before error encountered
Co-authored-by: jlelse <8822316+jlelse@users.noreply.github.com>
---
.github/workflows/docker.yml | 14 +++++++++-
.vscode/settings.json | 4 +--
.vscode/tasks.json | 11 +++++++-
activityPub_gotosocial_integration_test.go | 31 +++++++++++-----------
4 files changed, 40 insertions(+), 20 deletions(-)
diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml
index ea4e707c..5a401934 100644
--- a/.github/workflows/docker.yml
+++ b/.github/workflows/docker.yml
@@ -21,6 +21,18 @@ jobs:
tags: test
push: false
+ integration:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v6
+ - name: Set up Go
+ uses: actions/setup-go@v6
+ with:
+ go-version: 'stable'
+ - name: Run ActivityPub integration test
+ run: go test -tags=integration,linux,libsqlite3,sqlite_fts5 -run TestActivityPubWithGoToSocial ./...
+
build:
if: github.ref == 'refs/heads/master'
needs: test
@@ -73,4 +85,4 @@ jobs:
owner: ${{ github.repository_owner }}
package-type: 'container'
min-versions-to-keep: 0
- delete-only-untagged-versions: 'true'
\ No newline at end of file
+ delete-only-untagged-versions: 'true'
diff --git a/.vscode/settings.json b/.vscode/settings.json
index be0f8453..3a57f8d7 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -1,4 +1,4 @@
{
"go.buildTags": "linux,libsqlite3,sqlite_fts5",
- "go.testTags": "linux,libsqlite3,sqlite_fts5"
-}
\ No newline at end of file
+ "go.testTags": "linux,libsqlite3,sqlite_fts5,integration"
+}
diff --git a/.vscode/tasks.json b/.vscode/tasks.json
index 0f2c52de..b5bb7a0e 100644
--- a/.vscode/tasks.json
+++ b/.vscode/tasks.json
@@ -28,6 +28,15 @@
"kind": "test",
"isDefault": true
}
+ },
+ {
+ "label": "Test (integration)",
+ "type": "shell",
+ "command": "go test -tags=integration,linux,libsqlite3,sqlite_fts5 -run TestActivityPubWithGoToSocial ./...",
+ "group": {
+ "kind": "test",
+ "isDefault": false
+ }
}
]
-}
\ No newline at end of file
+}
diff --git a/activityPub_gotosocial_integration_test.go b/activityPub_gotosocial_integration_test.go
index fe32ba0a..6691c0bb 100644
--- a/activityPub_gotosocial_integration_test.go
+++ b/activityPub_gotosocial_integration_test.go
@@ -25,8 +25,8 @@ import (
)
const (
- gtsTestEmail = "gtsuser@example.com"
- gtsTestPassword = "GtsPassword123!@#"
+ gtsTestEmail = "gtsuser@example.com"
+ gtsTestPassword = "GtsPassword123!@#"
gtsContainerPort = "8080/tcp"
)
@@ -95,8 +95,8 @@ storage-local-base-path: "/data/storage"
WithPort(gtsContainerPort).
WithStartupTimeout(10 * time.Minute),
Mounts: testcontainers.Mounts(
- testcontainers.BindMount(gtsConfigPath, "/config/config.yaml"),
- testcontainers.BindMount(gtsDataDir, "/data"),
+ testcontainers.BindMount(gtsConfigPath, testcontainers.ContainerMountTarget("/config/config.yaml")),
+ testcontainers.BindMount(gtsDataDir, testcontainers.ContainerMountTarget("/data")),
),
ExtraHosts: []string{"host.docker.internal:host-gateway"},
}
@@ -136,9 +136,9 @@ storage-local-base-path: "/data/storage"
clientID, clientSecret := gtsRegisterApp(t, httpClient, gtsBaseURL)
accessToken := gtsAuthorizeToken(t, gtsBaseURL, clientID, clientSecret, gtsEmail, gtsPassword)
- goBlogAcct := fmt.Sprintf("%s@%s", app.cfg.DefaultBlog, app.cfg.Server.publicHostname)
+ goBlogActor := app.apIri(app.cfg.Blogs[app.cfg.DefaultBlog])
waitForHTTP(t, webfingerURL, 2*time.Minute)
- lookup := gtsLookupAccount(t, httpClient, gtsBaseURL, accessToken, goBlogAcct)
+ lookup := gtsLookupAccount(t, httpClient, gtsBaseURL, accessToken, goBlogActor)
gtsFollowAccount(t, httpClient, gtsBaseURL, accessToken, lookup.ID)
require.Eventually(t, func() bool {
@@ -239,23 +239,22 @@ type gtsLookupResponse struct {
func gtsLookupAccount(t *testing.T, client *http.Client, baseURL, token, acct string) gtsLookupResponse {
t.Helper()
query := url.Values{
- "acct": {acct},
+ "q": {fmt.Sprintf("@%s", acct)},
+ "resolve": {"true"},
}
- resp := doFormRequest(t, client, http.MethodGet, baseURL+"/api/v1/accounts/lookup?"+query.Encode(), nil, token)
+ resp := doFormRequest(t, client, http.MethodGet, baseURL+"/api/v1/accounts/search?"+query.Encode(), nil, token)
defer resp.Body.Close()
- if resp.StatusCode != http.StatusOK {
- return gtsLookupResponse{}
- }
- var payload gtsLookupResponse
+ require.Equal(t, http.StatusOK, resp.StatusCode)
+ var payload []gtsLookupResponse
require.NoError(t, json.NewDecoder(resp.Body).Decode(&payload))
- return payload
+ require.NotEmpty(t, payload)
+ require.NotEmpty(t, payload[0].ID)
+ return payload[0]
}
func gtsFollowAccount(t *testing.T, client *http.Client, baseURL, token, accountID string) {
t.Helper()
- if accountID == "" {
- t.Skip("gotosocial account lookup not available")
- }
+ require.NotEmpty(t, accountID)
resp := doFormRequest(t, client, http.MethodPost, fmt.Sprintf("%s/api/v1/accounts/%s/follow", baseURL, accountID), url.Values{}, token)
defer resp.Body.Close()
require.Contains(t, []int{http.StatusOK, http.StatusAccepted}, resp.StatusCode)