feat(server): revision authors, revisions after a seq, online machines
This commit is contained in:
1 parent
91d83f3032
commit
c2e9284a63
6 files changed
+299
-15
No files matched your search
@@ -0,0 +1,206 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"database/sql"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"lodge/internal/protocol"
|
||||
)
|
||||
|
||||
const (
|
||||
adminToken = "admin"
|
||||
repoKey = "0123456789abcdef0123456789abcdef01234567"
|
||||
)
|
||||
|
||||
func newServer(t *testing.T) string {
|
||||
t.Helper()
|
||||
store, err := OpenStore(filepath.Join(t.TempDir(), "lodge.db"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() { store.Close() })
|
||||
api := httptest.NewServer(New(store, Config{AdminToken: adminToken}))
|
||||
t.Cleanup(api.Close)
|
||||
return api.URL
|
||||
}
|
||||
|
||||
func call(t *testing.T, method, url, token string, in, out any) {
|
||||
t.Helper()
|
||||
var body bytes.Buffer
|
||||
if in != nil {
|
||||
_ = json.NewEncoder(&body).Encode(in)
|
||||
}
|
||||
req, _ := http.NewRequest(method, url, &body)
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
res, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode >= http.StatusBadRequest {
|
||||
t.Fatalf("%s %s: %s", method, url, res.Status)
|
||||
}
|
||||
if out != nil {
|
||||
if err := json.NewDecoder(res.Body).Decode(out); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func push(t *testing.T, url, author, path, base, content string) string {
|
||||
t.Helper()
|
||||
sum := sha256.Sum256([]byte(content))
|
||||
hash := hex.EncodeToString(sum[:])
|
||||
req, _ := http.NewRequest(http.MethodPut, url+"/api/blobs/"+hash, strings.NewReader(content))
|
||||
req.Header.Set("Authorization", "Bearer "+adminToken)
|
||||
res, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
res.Body.Close()
|
||||
change := protocol.Change{Space: protocol.SharedSpace, Path: path, Base: base, Hash: hash}
|
||||
call(t, http.MethodPost, url+"/api/repos/"+repoKey+"/revisions", adminToken, protocol.Push{Name: "vault", Author: author, Changes: []protocol.Change{change}}, nil)
|
||||
return hash
|
||||
}
|
||||
|
||||
func revisions(t *testing.T, url, query string) []protocol.Revision {
|
||||
t.Helper()
|
||||
var revisions []protocol.Revision
|
||||
call(t, http.MethodGet, url+"/api/repos/"+repoKey+"/revisions"+query, adminToken, nil, &revisions)
|
||||
return revisions
|
||||
}
|
||||
|
||||
func seqs(revisions []protocol.Revision) []int64 {
|
||||
seqs := make([]int64, len(revisions))
|
||||
for i, rev := range revisions {
|
||||
seqs[i] = rev.Seq
|
||||
}
|
||||
return seqs
|
||||
}
|
||||
|
||||
func TestRevisionsCarryAuthorAndPageAfterSeq(t *testing.T) {
|
||||
url := newServer(t)
|
||||
events, err := http.NewRequest(http.MethodGet, url+"/api/events", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
events.Header.Set("Authorization", "Bearer "+adminToken)
|
||||
stream, err := http.DefaultClient.Do(events)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer stream.Body.Close()
|
||||
|
||||
first := push(t, url, "", "a.md", "", "one")
|
||||
push(t, url, "alice", "a.md", first, "two")
|
||||
push(t, url, "bob", "b.md", "", "three")
|
||||
|
||||
if got := seqs(revisions(t, url, "")); len(got) != 3 || got[0] != 3 {
|
||||
t.Fatalf("default order = %v, want newest first", got)
|
||||
}
|
||||
after := revisions(t, url, "?after=1")
|
||||
if got := seqs(after); len(got) != 2 || got[0] != 2 || got[1] != 3 {
|
||||
t.Fatalf("after=1 = %v, want [2 3]", got)
|
||||
}
|
||||
if after[0].Author != "alice" || after[0].Machine != "web" || revisions(t, url, "?after=0")[0].Author != "" {
|
||||
t.Fatalf("authors = %+v", after)
|
||||
}
|
||||
if got := seqs(revisions(t, url, "?after=0&path=b.md")); len(got) != 1 || got[0] != 3 {
|
||||
t.Fatalf("after with path = %v", got)
|
||||
}
|
||||
if got := revisions(t, url, "?after=3"); len(got) != 0 {
|
||||
t.Fatalf("after the last revision = %v", got)
|
||||
}
|
||||
|
||||
var authors []string
|
||||
scanner := bufio.NewScanner(stream.Body)
|
||||
for len(authors) < 3 && scanner.Scan() {
|
||||
if data, ok := strings.CutPrefix(scanner.Text(), "data: "); ok {
|
||||
var event protocol.Event
|
||||
_ = json.Unmarshal([]byte(data), &event)
|
||||
authors = append(authors, event.Author)
|
||||
}
|
||||
}
|
||||
if strings.Join(authors, ",") != ",alice,bob" {
|
||||
t.Fatalf("event authors = %q", authors)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOldDatabaseGainsAuthorColumn(t *testing.T) {
|
||||
path := filepath.Join(t.TempDir(), "lodge.db")
|
||||
db, err := sql.Open("sqlite", path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, err = db.Exec(`
|
||||
create table revisions (repo text not null, seq integer not null, machine_id integer, time integer not null, main_sha text not null, main_branch text not null, primary key (repo, seq));
|
||||
create table changes (repo text not null, seq integer not null, space text not null, path text not null, base text not null, hash text not null, primary key (repo, seq, space, path));
|
||||
insert into revisions values ('` + repoKey + `', 1, null, 1, '', 'main');
|
||||
insert into changes values ('` + repoKey + `', 1, 'shared', 'a.md', '', 'x');`)
|
||||
if err := errors.Join(err, db.Close()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for range 2 {
|
||||
store, err := OpenStore(path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
revisions, err := store.Revisions(context.Background(), repoKey, "")
|
||||
store.Close()
|
||||
if err != nil || len(revisions) != 1 || revisions[0].Author != "" {
|
||||
t.Fatalf("old revisions = %+v (%v)", revisions, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMachinesShowWhoIsOnline(t *testing.T) {
|
||||
url := newServer(t)
|
||||
var code protocol.CodeResponse
|
||||
call(t, http.MethodPost, url+"/api/codes", adminToken, nil, &code)
|
||||
var claim protocol.ClaimResponse
|
||||
call(t, http.MethodPost, url+"/api/claim", "", protocol.ClaimRequest{Code: code.Code, Name: "dell"}, &claim)
|
||||
online := func() protocol.Machine {
|
||||
t.Helper()
|
||||
var machines []protocol.Machine
|
||||
call(t, http.MethodGet, url+"/api/machines", adminToken, nil, &machines)
|
||||
if len(machines) != 1 {
|
||||
t.Fatalf("machines = %+v", machines)
|
||||
}
|
||||
return machines[0]
|
||||
}
|
||||
if machine := online(); machine.Online || machine.LastSeen != 0 {
|
||||
t.Fatalf("a machine that never connected: %+v", machine)
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
req, _ := http.NewRequestWithContext(ctx, http.MethodGet, url+"/api/events", nil)
|
||||
req.Header.Set("Authorization", "Bearer "+claim.Token)
|
||||
stream, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if machine := online(); !machine.Online || machine.LastSeen == 0 {
|
||||
t.Fatalf("a machine listening for events: %+v", machine)
|
||||
}
|
||||
cancel()
|
||||
stream.Body.Close()
|
||||
deadline := time.Now().Add(5 * time.Second)
|
||||
for online().Online {
|
||||
if time.Now().After(deadline) {
|
||||
t.Fatal("the machine stays online after its stream closed")
|
||||
}
|
||||
time.Sleep(20 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user