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 "
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", "")
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: "", 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")
}