~bigbes/sr-ht-dolt

ref: 93a10f4843f643c67f30bd2605f4e6134cab324b sr-ht-dolt/cmd/dolt-git-hook/main_test.go -rw-r--r-- 4.3 KiB
93a10f48 — Eugene Blikh ci: pin Go caches inside the APKBUILD, not the env 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)
		}
	}
}