Skip to content
quarry Artifact [3472ce0c07]
Login

Artifact [3472ce0c07]

Artifact 3472ce0c07b7453b3c8675b4d717a91b3937b98bb61e7bf5c5e51d3409579326:


package api

import (
	"context"
	"errors"
	"path/filepath"
	"strconv"
	"strings"
	"testing"
	"time"

	"craftdesign.group/quarry/internal/attach"
	"craftdesign.group/quarry/internal/oltp"
	"craftdesign.group/quarry/internal/ops"
)

// A chat line carries the files pasted into it: begun with family chat
// under the chat letter, checked before the line is posted, named in
// its text as attach:NAME and resolved against that line alone, and
// gone with the line when it is deleted.
func TestAChatLineCarriesItsFiles(t *testing.T) {
	ix, z, _ := attachStore(t)
	srv, objects := newObjectS3(t)
	buckets := newMemBuckets()
	buckets.rows["test"] = oltp.BucketBinding{Endpoint: srv.URL, Bucket: "files", Region: "auto", AccessKey: "AK", Secret: "SK",
		LimitBytes: 1 << 20, DeleteObjects: true}
	db, err := oltp.Open(filepath.Join(t.TempDir(), "o.db"))
	if err != nil {
		t.Fatal(err)
	}
	t.Cleanup(func() { db.Close() })
	d := Deps{Version: "v", Repos: NewRepos(map[string]*Repo{"test": {Index: ix, Zit: z}}),
		Chat: &fakeChat{}, Buckets: buckets, Attachments: newMemAttachments(buckets), ChatFiles: db}
	r := ops.NewRegistry()
	if err := RegisterAll(r, d); err != nil {
		t.Fatal(err)
	}
	begin := func(who, letters string) (attachmentBeginOut, error) {
		out, err := r.Invoke(as(who, letters), "attachment.begin", attachmentBeginIn{Repo: "test", Family: "chat",
			Name: "shot.png", Size: 500, Type: "image/png", SHA256: sha})
		if err != nil {
			return attachmentBeginOut{}, err
		}
		return out.(attachmentBeginOut), nil
	}
	post := func(who, text string, tickets ...string) (uint64, error) {
		out, err := r.Invoke(as(who, "C"), "chat.post", chatPostIn{Repo: "test", Text: text, Attach: tickets})
		if err != nil {
			return 0, err
		}
		return out.(chatPostOut).Seq, nil
	}
	if _, err := begin("lou", "bB"); !errors.Is(err, ops.ErrForbidden) {
		t.Errorf("FAIL: a chat upload began without the chat letter: %v", err)
	}
	b, err := begin("lou", "C")
	if err != nil {
		t.Fatalf("FAIL: begin for chat: %v", err)
	}
	if b.Name != "shot.png" {
		t.Errorf("FAIL: begin named the file %q", b.Name)
	}
	line := "look ![shot](attach:shot.png)"
	if _, err := post("lou", line, b.Ticket); !errors.Is(err, ops.ErrInvalid) {
		t.Errorf("FAIL: a line was posted with a file that never reached the bucket: %v", err)
	}
	objects.objects[attach.Key(sha)] = 500
	if _, err := post("ann", line, b.Ticket); !errors.Is(err, ops.ErrForbidden) {
		t.Errorf("FAIL: someone else's upload rode on a line: %v", err)
	}
	if n := len(d.Chat.(*fakeChat).msgs); n != 0 {
		t.Fatalf("FAIL: %d line(s) posted by refused sends", n)
	}
	seq, err := post("lou", line, b.Ticket)
	if err != nil {
		t.Fatalf("FAIL: post with its file: %v", err)
	}
	if _, ok, _ := d.Attachments.UploadTicket(b.Ticket); ok {
		t.Error("FAIL: the ticket was not spent")
	}
	got, err := r.Invoke(as("ann", "C"), "chat.get", chatGetIn{Repo: "test", Seq: seq})
	if err != nil {
		t.Fatal(err)
	}
	html := got.(chatGetOut).Message.HTML
	if want := `/r/test/chat/` + strconv.FormatUint(seq, 10) + `/file/shot.png`; !strings.Contains(html, want) {
		t.Errorf("FAIL: the picture does not point at the line's file %s: %s", want, html)
	}
	// Another line naming the same file holds none of its own.
	other, err := post("ann", "![shot](attach:shot.png)")
	if err != nil {
		t.Fatal(err)
	}
	if _, err := r.Invoke(as("ann", "C"), "chat.file", chatFileIn{Repo: "test", Seq: other, Name: "shot.png"}); !errors.Is(err, ops.ErrNotFound) {
		t.Errorf("FAIL: a line answered for a file it does not carry: %v", err)
	}
	link, err := r.Invoke(as("ann", "C"), "chat.file", chatFileIn{Repo: "test", Seq: seq, Name: "shot.png"})
	if err != nil {
		t.Fatalf("FAIL: chat.file: %v", err)
	}
	if l := link.(chatFileOut); l.Download == "" || l.Inline == "" || l.Type != "image/png" {
		t.Errorf("FAIL: chat.file = %+v", l)
	}
	if _, err := r.Invoke(as("lou", "C"), "chat.delete", chatDeleteIn{Repo: "test", Seq: seq}); err != nil {
		t.Fatal(err)
	}
	if _, err := r.Invoke(as("ann", "C"), "chat.file", chatFileIn{Repo: "test", Seq: seq, Name: "shot.png"}); !errors.Is(err, ops.ErrNotFound) {
		t.Errorf("FAIL: a deleted line's file still answers: %v", err)
	}
	if len(objects.deleted) != 1 || objects.deleted[0] != attach.Key(sha) {
		t.Errorf("FAIL: deleting the line deleted %v from the bucket", objects.deleted)
	}
	t.Log("ok: a chat line's files are checked, resolved against it, and go with it")
}

// A file pasted into chat and never sent leaves no bytes behind (#380):
// once its ticket expires unspent the sweep deletes the object, under
// the deleteObjects switch, and never one a sent line still carries.
func TestUnsentChatFilesAreSwept(t *testing.T) {
	ix, z, _ := attachStore(t)
	srv, objects := newObjectS3(t)
	buckets := newMemBuckets()
	buckets.rows["test"] = oltp.BucketBinding{Endpoint: srv.URL, Bucket: "files", Region: "auto", AccessKey: "AK", Secret: "SK",
		LimitBytes: 1 << 20, DeleteObjects: true}
	db, err := oltp.Open(filepath.Join(t.TempDir(), "o.db"))
	if err != nil {
		t.Fatal(err)
	}
	t.Cleanup(func() { db.Close() })
	store := newMemAttachments(buckets)
	d := Deps{Version: "v", Repos: NewRepos(map[string]*Repo{"test": {Index: ix, Zit: z}}),
		Chat: &fakeChat{}, Buckets: buckets, Attachments: store, ChatFiles: db}
	r := ops.NewRegistry()
	if err := RegisterAll(r, d); err != nil {
		t.Fatal(err)
	}
	begin := func(name, hash string) string {
		t.Helper()
		out, err := r.Invoke(as("lou", "C"), "attachment.begin", attachmentBeginIn{Repo: "test", Family: "chat",
			Name: name, Size: 500, Type: "image/png", SHA256: hash})
		if err != nil {
			t.Fatal(err)
		}
		objects.objects[attach.Key(hash)] = 500
		return out.(attachmentBeginOut).Ticket
	}
	age := func(id string) {
		store.mu.Lock()
		tk := store.tickets[id]
		tk.Created = time.Now().Add(-2 * oltp.UploadTicketTTL).UTC().Format(time.RFC3339)
		store.tickets[id] = tk
		store.mu.Unlock()
	}
	other := strings.Repeat("ab", 32)
	unsent := begin("left.png", other)
	sent := begin("shot.png", sha)
	if _, err := r.Invoke(as("lou", "C"), "chat.post", chatPostIn{Repo: "test", Text: "![shot](attach:shot.png)", Attach: []string{sent}}); err != nil {
		t.Fatal(err)
	}
	// The same bytes as the sent picture, pasted again and taken out.
	again := begin("shot-again.png", sha)
	age(unsent)
	age(again)
	d.ChatUploadSweep(context.Background())
	if len(objects.deleted) != 1 || objects.deleted[0] != attach.Key(other) {
		t.Fatalf("FAIL: the sweep deleted %v, want only the unsent file's %s", objects.deleted, attach.Key(other))
	}
	t.Log("ok: an unsent chat file's bytes are swept; a sent line's are kept")
}