~bigbes/sr-ht-dolt

ref: 8527f0fdd75341f817d55c259583dd1dbebe173f sr-ht-dolt/cmd/dolt-git-hook/main_test.go -rw-r--r-- 4.3 KiB
8527f0fd — Eugene Blikh feat(web/beads): hierarchy in the milestone view 13 days ago
                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
package main

import (
	"bytes"
	"crypto/ed25519"
	"encoding/base64"
	"encoding/json"
	"io"
	"net/http"
	"net/http/httptest"
	"strings"
	"testing"
	"time"

	"github.com/fernet/fernet-go"
	"github.com/vaughan0/go-ini"

	"sourcecraft.dev/bigbes/sr-ht-core/crypto"
)

// initTestCrypto installs a random network key + webhooks seed into the shared
// crypto globals, mirroring the sr-ht-core test pattern, so createCompanion's
// crypto.Encrypt and the test server's DecryptWithExpiration share a keyset.
func initTestCrypto(t *testing.T) {
	t.Helper()
	var fk fernet.Key
	if err := fk.Generate(); err != nil {
		t.Fatalf("fernet generate: %v", err)
	}
	seed := make([]byte, ed25519.SeedSize)
	for i := range seed {
		seed[i] = byte(i + 1)
	}
	crypto.InitCrypto(ini.File{
		"sr.ht":    ini.Section{"network-key": fk.Encode()},
		"webhooks": ini.Section{"private-key": base64.StdEncoding.EncodeToString(seed)},
	})
}

func samplePush() pushContext {
	var pc pushContext
	pc.Repo.Name = "widgets"
	pc.Repo.OwnerName = "alice"
	pc.Repo.Visibility = "private"
	return pc
}

// decodeInternalAuth verifies an incoming "Internal <token>" header the same way
// dolt.sr.ht's internalAuthGuard does and returns the decoded claims.
func decodeInternalAuth(t *testing.T, r *http.Request) internalAuth {
	t.Helper()
	parts := strings.SplitN(r.Header.Get("Authorization"), " ", 2)
	if len(parts) != 2 || !strings.EqualFold(parts[0], "internal") {
		t.Fatalf("missing/invalid Internal auth header: %q", r.Header.Get("Authorization"))
	}
	payload := crypto.DecryptWithExpiration([]byte(parts[1]), 30*time.Second)
	if payload == nil {
		t.Fatalf("auth token did not decrypt (wrong key or expired)")
	}
	var ia internalAuth
	if err := json.Unmarshal(payload, &ia); err != nil {
		t.Fatalf("auth payload not JSON: %v", err)
	}
	return ia
}

func TestCreateCompanionSignsAndAnnouncesOnCreate(t *testing.T) {
	initTestCrypto(t)

	var gotBody map[string]string
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.URL.Path != "/internal/repos" || r.Method != http.MethodPost {
			t.Errorf("unexpected request %s %s", r.Method, r.URL.Path)
		}
		ia := decodeInternalAuth(t, r)
		if ia.ClientID != "git.sr.ht" || ia.NodeID != "dolt-git-hook" {
			t.Errorf("unexpected auth claims: %+v", ia)
		}
		raw, _ := io.ReadAll(r.Body)
		_ = json.Unmarshal(raw, &gotBody)
		w.WriteHeader(http.StatusCreated)
		_, _ = w.Write([]byte(`{"url":"https://dolt.example/~alice/widgets","created":true}`))
	}))
	defer srv.Close()

	var out bytes.Buffer
	createCompanion(&out, srv.URL, samplePush())

	if gotBody["owner"] != "alice" || gotBody["name"] != "widgets" {
		t.Fatalf("unexpected request body: %+v", gotBody)
	}
	if gotBody["visibility"] != "PRIVATE" {
		t.Fatalf("visibility not normalized to PRIVATE: %q", gotBody["visibility"])
	}
	if !strings.Contains(out.String(), "NOTICE") ||
		!strings.Contains(out.String(), "dolt clone https://dolt.example/~alice/widgets") {
		t.Fatalf("expected clone notice, got: %q", out.String())
	}
}

func TestCreateCompanionQuietWhenExists(t *testing.T) {
	initTestCrypto(t)
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		decodeInternalAuth(t, r) // still must be authenticated
		w.WriteHeader(http.StatusOK)
		_, _ = w.Write([]byte(`{"url":"https://dolt.example/~alice/widgets","created":false}`))
	}))
	defer srv.Close()

	var out bytes.Buffer
	createCompanion(&out, srv.URL, samplePush())
	if out.Len() != 0 {
		t.Fatalf("expected no output for existing companion, got: %q", out.String())
	}
}

func TestCreateCompanionWarnsOnError(t *testing.T) {
	initTestCrypto(t)
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(http.StatusInternalServerError)
		_, _ = w.Write([]byte("boom"))
	}))
	defer srv.Close()

	var out bytes.Buffer
	createCompanion(&out, srv.URL, samplePush())
	if !strings.Contains(out.String(), "provisioning failed (500)") {
		t.Fatalf("expected failure warning, got: %q", out.String())
	}
}

func TestNormalizeVisibility(t *testing.T) {
	cases := map[string]string{
		"private": "PRIVATE", "PUBLIC": "PUBLIC", "Unlisted": "UNLISTED",
		"": "", "bogus": "",
	}
	for in, want := range cases {
		if got := normalizeVisibility(in); got != want {
			t.Errorf("normalizeVisibility(%q) = %q, want %q", in, got, want)
		}
	}
}