package gitcode
import (
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"strings"
"testing"
)
func TestListPushRemoteMirrorsUsesV5RouteAndSanitizes(t *testing.T) {
const secret = "credential-value-that-must-not-leak"
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
t.Errorf("method=%s want GET", r.Method)
}
if r.URL.EscapedPath() != "/api/v5/repos/example-owner/example-repo/push_remote_mirrors" {
t.Errorf("path=%q", r.URL.EscapedPath())
}
if r.URL.RawQuery != "" {
t.Errorf("query=%q want empty", r.URL.RawQuery)
}
if r.Header.Get("Authorization") != "Bearer test-token" {
t.Errorf("authorization header=%q", r.Header.Get("Authorization"))
}
if r.Header.Get("Accept") != "application/json" {
t.Errorf("accept=%q", r.Header.Get("Accept"))
}
for _, header := range []string{"Cookie", "Origin", "Referer", "X-Device-ID", "page-uri"} {
if value := r.Header.Get(header); value != "" {
t.Errorf("%s=%q want absent", header, value)
}
}
w.Header().Set("Content-Type", "application/json")
fmt.Fprintf(w, `[{
"id": 17,
"project_id": 101,
"url": "https://user:%s@mirror.example.invalid/example/repo.git?access_token=%s&ref=main#secret",
"force": true,
"is_private": true,
"update_status": "finished",
"number_of_failures": 2,
"message": "retry https://user:%s@mirror.example.invalid/example/repo.git?token=%s",
"created_at": "2026-07-29T09:00:00Z",
"last_update_at": "2026-07-29T10:00:00Z",
"last_successful_update_at": "2026-07-29T10:00:00Z"
}]`, secret, secret, secret, secret)
}))
defer server.Close()
client := newTestClient(t, server.URL, Config{Token: "test-token"})
mirrors, err := client.ListPushRemoteMirrors(context.Background(), PushMirrorListRequest{Owner: "example-owner", Repo: "example-repo"})
if err != nil {
t.Fatalf("ListPushRemoteMirrors: %v", err)
}
if len(mirrors) != 1 {
t.Fatalf("mirrors=%#v", mirrors)
}
mirror := mirrors[0]
if mirror.RemoteID != "17" || mirror.ProjectID != "101" {
t.Fatalf("ids=%#v", mirror)
}
if mirror.URL != "https://mirror.example.invalid/example/repo.git" {
t.Fatalf("url=%q", mirror.URL)
}
if mirror.Message != "retry https://mirror.example.invalid/example/repo.git" {
t.Fatalf("message=%q", mirror.Message)
}
if !mirror.Force || !mirror.Private || mirror.UpdateStatus != "finished" || mirror.NumberOfFailures != 2 {
t.Fatalf("mirror=%#v", mirror)
}
if strings.Contains(fmt.Sprintf("%+v", mirror), secret) {
t.Fatalf("sanitized mirror leaked secret: %+v", mirror)
}
}
func TestListPushRemoteMirrorsFixtureShape(t *testing.T) {
fixture, err := os.ReadFile(filepath.Join("..", "..", "testdata", "gitcode", "push-remote-mirrors-v5.json"))
if err != nil {
t.Fatal(err)
}
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write(fixture)
}))
defer server.Close()
client := newTestClient(t, server.URL, Config{Token: "test-token"})
mirrors, err := client.ListPushRemoteMirrors(context.Background(), PushMirrorListRequest{Owner: "example-owner", Repo: "example-repo"})
if err != nil {
t.Fatalf("ListPushRemoteMirrors: %v", err)
}
if len(mirrors) != 1 || mirrors[0].RemoteID != "17" || mirrors[0].URL != "https://mirror.example.invalid/example-owner/example-repo.git" {
t.Fatalf("mirrors=%#v", mirrors)
}
}
func TestListPushRemoteMirrorsEmptyList(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
_, _ = w.Write([]byte(`[]`))
}))
defer server.Close()
client := newTestClient(t, server.URL, Config{Token: "test-token"})
mirrors, err := client.ListPushRemoteMirrors(context.Background(), PushMirrorListRequest{Owner: "example-owner", Repo: "example-repo"})
if err != nil {
t.Fatalf("ListPushRemoteMirrors: %v", err)
}
if len(mirrors) != 0 {
t.Fatalf("mirrors=%#v want empty", mirrors)
}
}
func TestListPushRemoteMirrorsErrorsAreTyped(t *testing.T) {
tests := []struct {
name string
status int
body string
check func(error) bool
}{
{name: "unauthorized", status: http.StatusUnauthorized, body: `{"message":"unauthorized"}`, check: func(err error) bool {
var target ErrAuthExpired
return errors.As(err, &target)
}},
{name: "forbidden", status: http.StatusForbidden, body: `{"message":"forbidden"}`, check: func(err error) bool {
var target ErrForbidden
return errors.As(err, &target)
}},
{name: "malformed", status: http.StatusOK, body: `[{"id":`, check: func(err error) bool {
var target ErrPartialResponse
return errors.As(err, &target)
}},
{name: "html-login", status: http.StatusOK, body: `<!doctype html><title>login</title>`, check: func(err error) bool {
var target ErrUnexpectedContentType
return errors.As(err, &target)
}},
{name: "invalid-timestamp", status: http.StatusOK, body: `[{"id":1,"url":"https://mirror.example.invalid/repo.git","last_update_at":"not-a-time"}]`, check: func(err error) bool {
var target *ErrSchemaDecode
return errors.As(err, &target)
}},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(test.status)
_, _ = w.Write([]byte(test.body))
}))
defer server.Close()
client := newTestClient(t, server.URL, Config{Token: "test-token"})
_, err := client.ListPushRemoteMirrors(context.Background(), PushMirrorListRequest{Owner: "example-owner", Repo: "example-repo"})
if err == nil || !test.check(err) {
t.Fatalf("error=%T %v", err, err)
}
})
}
}
func TestListPushRemoteMirrorsInvalidDestinationIsRedacted(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
_, _ = w.Write([]byte(`[{"id":1,"project_id":2,"url":"not a safe remote"}]`))
}))
defer server.Close()
client := newTestClient(t, server.URL, Config{Token: "test-token"})
mirrors, err := client.ListPushRemoteMirrors(context.Background(), PushMirrorListRequest{Owner: "example-owner", Repo: "example-repo"})
if err != nil {
t.Fatalf("ListPushRemoteMirrors: %v", err)
}
if len(mirrors) != 1 || mirrors[0].URL != redactedPushMirrorDestination {
t.Fatalf("mirrors=%#v", mirrors)
}
}
func TestTriggerPushRemoteMirrorUsesV5PostAndSanitizedReadback(t *testing.T) {
const secret = "credential-value-that-must-not-leak"
var postCalls int
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodPost:
postCalls++
if r.URL.EscapedPath() != "/api/v5/repos/example-owner/example-repo/push_remote_mirrors/17" {
t.Fatalf("post path=%q", r.URL.EscapedPath())
}
if r.Header.Get("Idempotency-Key") != "trigger-key" {
t.Fatalf("idempotency=%q", r.Header.Get("Idempotency-Key"))
}
var body map[string]any
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
t.Fatal(err)
}
if len(body) != 1 || body["force"] != true {
t.Fatalf("body=%#v", body)
}
_, _ = w.Write([]byte(`{"message":"accepted"}`))
case http.MethodGet:
fmt.Fprintf(w, `[{"id":17,"project_id":101,"url":"https://user:%s@mirror.example.invalid/repo.git?token=%s#fragment","update_status":"processing","last_update_at":"2026-07-30T12:00:00Z"}]`, secret, secret)
default:
t.Fatalf("method=%s", r.Method)
}
}))
defer server.Close()
client := newTestClient(t, server.URL, Config{Token: "test-token", MaxRetries: 3})
result, err := client.TriggerPushRemoteMirror(context.Background(), PushMirrorTriggerRequest{Owner: "example-owner", Repo: "example-repo", MirrorID: "17"}, WriteOptions{IdempotencyKey: "trigger-key"})
if err != nil {
t.Fatalf("TriggerPushRemoteMirror: %v", err)
}
if postCalls != 1 || !result.Confirmed || result.RemoteID != "17" || result.Record.UpdateStatus != "processing" {
t.Fatalf("postCalls=%d result=%#v", postCalls, result)
}
if result.Record.URL != "https://mirror.example.invalid/repo.git" || strings.Contains(fmt.Sprintf("%+v", result.Record), secret) {
t.Fatalf("record=%#v", result.Record)
}
}
func TestTriggerPushRemoteMirrorMapsCooldownAndDoesNotRetry(t *testing.T) {
var calls int
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
calls++
w.WriteHeader(http.StatusForbidden)
_, _ = w.Write([]byte(`{"error_message":"Sync too frequently, please try again later"}`))
}))
defer server.Close()
client := newTestClient(t, server.URL, Config{Token: "test-token", MaxRetries: 3})
_, err := client.TriggerPushRemoteMirror(context.Background(), PushMirrorTriggerRequest{Owner: "example-owner", Repo: "example-repo", MirrorID: "17"}, WriteOptions{IdempotencyKey: "trigger-key"})
var target ErrPushMirrorSyncInProgress
if !errors.As(err, &target) {
t.Fatalf("error=%T %v", err, err)
}
if calls != 1 {
t.Fatalf("calls=%d want 1", calls)
}
}
func TestTriggerPushRemoteMirrorServerFailureIsNotRetried(t *testing.T) {
var calls int
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
calls++
w.WriteHeader(http.StatusBadGateway)
}))
defer server.Close()
client := newTestClient(t, server.URL, Config{Token: "test-token", MaxRetries: 3})
_, err := client.TriggerPushRemoteMirror(context.Background(), PushMirrorTriggerRequest{Owner: "example-owner", Repo: "example-repo", MirrorID: "17"}, WriteOptions{IdempotencyKey: "trigger-key"})
var target ErrNetworkUnavailable
if !errors.As(err, &target) {
t.Fatalf("error=%T %v", err, err)
}
if calls != 1 {
t.Fatalf("calls=%d want 1", calls)
}
}