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
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
|
package main
import (
"testing"
)
// Content decides the type, not the file name — a JPEG called .png is still a
// JPEG, and the pipeline that follows would decode it as one either way.
func TestSniffUploadPrefersContent(t *testing.T) {
jpegData := encodeJPEG(t, testImage(8, 8))
det, err := sniffUpload("misnamed.png", jpegData)
if err != nil {
t.Fatalf("sniffUpload: %v", err)
}
if det.MIME != "image/jpeg" {
t.Errorf("MIME = %q, want image/jpeg", det.MIME)
}
}
func TestSniffUploadTypes(t *testing.T) {
cases := []struct {
name string
filename string
data []byte
wantKind string
wantBlock string
}{
{"png", "a.png", encodePNG(t, testImage(4, 4)), AssetImage, KindImage},
{"jpeg", "a.jpg", encodeJPEG(t, testImage(4, 4)), AssetImage, KindImage},
{"binary stl", "a.stl", binaryCubeSTL(10, true), AssetSTL, KindSTL},
{"ascii stl", "a.stl", asciiCubeSTL(10), AssetSTL, KindSTL},
// An STL with no useful extension still parses, so it is still accepted.
{"stl without extension", "model", binaryCubeSTL(10, true), AssetSTL, KindSTL},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
det, err := sniffUpload(c.filename, c.data)
if err != nil {
t.Fatalf("sniffUpload: %v", err)
}
if det.AssetKind != c.wantKind || det.BlockKind != c.wantBlock {
t.Errorf("got (%s, %s), want (%s, %s)",
det.AssetKind, det.BlockKind, c.wantKind, c.wantBlock)
}
})
}
}
func TestSniffUploadRejectsUnsupported(t *testing.T) {
for name, data := range map[string][]byte{
"pdf": []byte("%PDF-1.7\nsome pdf content"),
"text": []byte("just a text file"),
"gif": []byte("GIF89a\x00\x00\x00\x00"),
} {
t.Run(name, func(t *testing.T) {
if _, err := sniffUpload("file."+name, data); err == nil {
t.Error("expected the upload to be rejected")
}
})
}
}
func TestIngestUploadCreatesBlock(t *testing.T) {
requireCWebP(t)
db := testDB(t)
postID, _ := seed(t, db, 2)
// Drop onto the first block: the new block must land immediately after it.
b, err := ingestUpload(db, postID, 0, "photo.png", encodePNG(t, testImage(40, 20)))
if err != nil {
t.Fatalf("ingestUpload: %v", err)
}
if b.Kind != KindImage {
t.Errorf("kind = %q, want %q", b.Kind, KindImage)
}
blocks, err := blocksOfPost(db, postID)
if err != nil {
t.Fatal(err)
}
if len(blocks) != 3 || blocks[1].Kind != KindImage {
t.Fatalf("new block did not land after the drop target: %v", contents(t, db, postID))
}
if blocks[1].Asset == nil || blocks[1].Asset.Width != 40 {
t.Errorf("asset not attached with its dimensions: %+v", blocks[1].Asset)
}
}
func TestIngestUploadSTL(t *testing.T) {
db := testDB(t)
postID, _ := seed(t, db, 1)
b, err := ingestUpload(db, postID, 0, "part.stl", binaryCubeSTL(25, true))
if err != nil {
t.Fatalf("ingestUpload: %v", err)
}
if b.Kind != KindSTL {
t.Errorf("kind = %q, want %q", b.Kind, KindSTL)
}
// The packed mesh must be stored, since that is what the viewer fetches.
rend, err := loadRendition(db, *b.AssetID, VariantMesh)
if err != nil {
t.Fatalf("mesh rendition: %v", err)
}
if want := 12 * 3 * vertexStride * 4; len(rend.Bytes) != want {
t.Errorf("mesh = %d bytes, want %d", len(rend.Bytes), want)
}
}
// Assets are content-addressed, so dropping the same file twice must not store
// its bytes twice.
func TestDuplicateUploadsShareOneAsset(t *testing.T) {
requireCWebP(t)
db := testDB(t)
postID, _ := seed(t, db, 1)
data := encodePNG(t, testImage(30, 30))
first, err := ingestUpload(db, postID, 0, "same.png", data)
if err != nil {
t.Fatal(err)
}
second, err := ingestUpload(db, postID, 1, "same-again.png", data)
if err != nil {
t.Fatal(err)
}
if *first.AssetID != *second.AssetID {
t.Errorf("identical uploads produced assets %d and %d", *first.AssetID, *second.AssetID)
}
var n int
if err := db.QueryRow(`SELECT COUNT(*) FROM asset`).Scan(&n); err != nil {
t.Fatal(err)
}
if n != 1 {
t.Errorf("%d asset rows, want 1", n)
}
}
// A rejected upload must leave no partial state behind.
func TestFailedUploadCreatesNothing(t *testing.T) {
db := testDB(t)
postID, _ := seed(t, db, 1)
if _, err := ingestUpload(db, postID, 0, "notes.txt", []byte("not a supported file")); err == nil {
t.Fatal("expected the upload to be rejected")
}
if got := contents(t, db, postID); len(got) != 1 {
t.Errorf("block list changed: %v", got)
}
var n int
if err := db.QueryRow(`SELECT COUNT(*) FROM asset`).Scan(&n); err != nil {
t.Fatal(err)
}
if n != 0 {
t.Errorf("%d assets stored despite the failure", n)
}
}
// An upload naming a post that does not exist must not leave a stored blob
// behind — the asset is written before the block, so the post is checked first.
func TestUploadToMissingPostStoresNothing(t *testing.T) {
requireCWebP(t)
db := testDB(t)
if _, err := ingestUpload(db, 9999, 0, "x.png", encodePNG(t, testImage(20, 20))); err == nil {
t.Fatal("expected an error for a missing post")
}
var n int
if err := db.QueryRow(`SELECT COUNT(*) FROM asset`).Scan(&n); err != nil {
t.Fatal(err)
}
if n != 0 {
t.Errorf("%d orphaned assets stored", n)
}
}
func TestEmptyUploadRejected(t *testing.T) {
db := testDB(t)
postID, _ := seed(t, db, 1)
if _, err := ingestUpload(db, postID, 0, "empty.png", nil); err == nil {
t.Error("expected an empty file to be rejected")
}
}
|