github.com/raphaelreyna/latte@v0.11.2-0.20220317193248-98e2fcef4eef/internal/server/generate-route_test.go (about) 1 package server 2 3 import ( 4 "bytes" 5 "context" 6 "encoding/base64" 7 "encoding/json" 8 "errors" 9 "fmt" 10 "io/ioutil" 11 "log" 12 "net/http/httptest" 13 "net/url" 14 "os" 15 "path/filepath" 16 "testing" 17 "github.com/raphaelreyna/latte/internal/job" 18 ) 19 20 type mockDB struct { 21 data map[string]interface{} 22 } 23 24 func (mdb *mockDB) Store(ctx context.Context, uid string, i interface{}) error { 25 mdb.data[uid] = i 26 return nil 27 } 28 29 func (mdb *mockDB) Fetch(ctx context.Context, uid string) (interface{}, error) { 30 result, exists := mdb.data[uid] 31 if !exists { 32 return nil, errors.New("file not found") 33 } 34 return result, nil 35 } 36 37 func (mdb *mockDB) Ping(ctx context.Context) error { 38 return nil 39 } 40 41 func (mdb *mockDB) AddFileAs(name, destination string, perm os.FileMode) error { 42 log.Println("adding file from db") 43 file, err := os.OpenFile(destination, os.O_CREATE|os.O_WRONLY, perm) 44 if err != nil { 45 return err 46 } 47 defer file.Close() 48 49 data, exists := mdb.data[name] 50 if !exists { 51 os.Remove(file.Name()) 52 log.Println("could not find file") 53 return fmt.Errorf("could not find file") 54 } 55 56 dataString := string(data.([]uint8)) 57 58 _, err = file.Write([]byte(dataString)) 59 60 return err 61 } 62 63 // TestHandleGenerate_Basic tests the end product PDF of a generate request. 64 func TestHandleGenerate_Basic(t *testing.T) { 65 err := os.Chdir("../../testing") 66 if err != nil { 67 t.Fatalf("error while moving into testing directory: %+v", err) 68 } 69 70 type test struct { 71 // Name provides a short description of the test case 72 Name string 73 74 // Name of the .pdf file in the testing pdf assets folder to test final product against 75 Expectation string 76 ExpectedToPass bool 77 ExpectedStatusCode int 78 79 // Name of .tex file in the testing tex assets folder 80 TexFile string 81 TexFileRegLevel int // 0 - unregistered; 1 - registered and on disk; 2 - registered and in db and not on disk 82 83 // Name of .json file in the testing details assets folder 84 DtlsFile string 85 DtlsFileRegLevel int // 0 - unregistered; 1 - registered and on disk; 2 - registered and in db and not on disk 86 87 // List of resource file names in the testing resources assets folder 88 Resources []string 89 ResourcesRegLevel int // 0 - unregistered; 1 - registered and on disk; 2 - registered and in db and not on disk 90 91 // Needs to have keys "left" and "right", both of which have values which are two character strings 92 Delimiters map[string]string 93 94 // OnMissingKey valid values: 'error', 'zero', 'nothing' 95 OnMissingKey string 96 97 // Compiler valid values: "pdflatex", "latexmk" 98 Compiler string 99 100 // Count valid values: > 0 101 Count uint 102 } 103 104 tt := []test{ 105 test{ 106 Name: "Basic", 107 TexFile: "hello-world.tex", 108 DtlsFile: "hello-world_alice.json", 109 Resources: nil, 110 Delimiters: map[string]string{"left": "#!", "right": "!#"}, 111 Expectation: "hello-world_alice.pdf", 112 ExpectedToPass: true, 113 }, 114 test{ 115 Name: "Basic with multiple recompiles", 116 TexFile: "hello-world.tex", 117 DtlsFile: "hello-world_alice.json", 118 Resources: nil, 119 Delimiters: map[string]string{"left": "#!", "right": "!#"}, 120 Count: 4, 121 Expectation: "hello-world_alice.pdf", 122 ExpectedToPass: true, 123 }, 124 test{ 125 Name: "Basic with latexmk", 126 TexFile: "hello-world.tex", 127 DtlsFile: "hello-world_alice.json", 128 Resources: nil, 129 Delimiters: map[string]string{"left": "#!", "right": "!#"}, 130 Compiler: "latexmk", 131 Expectation: "hello-world_alice.pdf", 132 ExpectedToPass: true, 133 }, 134 test{ 135 Name: "Registered tex file", 136 TexFile: "hello-world.tex", 137 TexFileRegLevel: 1, 138 DtlsFile: "hello-world_alice.json", 139 Resources: nil, 140 Delimiters: map[string]string{"left": "#!", "right": "!#"}, 141 Expectation: "hello-world_alice.pdf", 142 ExpectedToPass: true, 143 }, 144 test{ 145 Name: "Registered tex file in db", 146 TexFile: "hello-world.tex", 147 TexFileRegLevel: 2, 148 DtlsFile: "hello-world_alice.json", 149 Resources: nil, 150 Delimiters: map[string]string{"left": "#!", "right": "!#"}, 151 Expectation: "hello-world_alice.pdf", 152 ExpectedToPass: true, 153 }, 154 test{ 155 Name: "Registered details file", 156 TexFile: "hello-world.tex", 157 DtlsFile: "hello-world_alice.json", 158 DtlsFileRegLevel: 1, 159 Resources: nil, 160 Delimiters: map[string]string{"left": "#!", "right": "!#"}, 161 Expectation: "hello-world_alice.pdf", 162 ExpectedToPass: true, 163 }, 164 test{ 165 Name: "Registered details file in db", 166 TexFile: "hello-world.tex", 167 DtlsFile: "hello-world_alice.json", 168 DtlsFileRegLevel: 2, 169 Resources: nil, 170 Delimiters: map[string]string{"left": "#!", "right": "!#"}, 171 Expectation: "hello-world_alice.pdf", 172 ExpectedToPass: true, 173 }, 174 test{ 175 Name: "Wrong details file", 176 TexFile: "hello-world.tex", 177 DtlsFile: "hello-world_wrong-field.json", 178 Delimiters: map[string]string{"left": "#!", "right": "!#"}, 179 OnMissingKey: "error", 180 Resources: nil, 181 ExpectedToPass: false, 182 }, 183 } 184 185 // Create temp dir for testing 186 testingDir, err := ioutil.TempDir("./", "testingTmp") 187 if err != nil { 188 t.Fatal("error creating root testingTmp directory") 189 } 190 err = os.Chdir(testingDir) 191 if err != nil { 192 t.Fatal("error moving into testingTmp directory") 193 } 194 defer func() { 195 os.Chdir("../") 196 // os.RemoveAll(testingDir) 197 }() 198 199 for _, tc := range tt { 200 t.Run(tc.Name, func(t *testing.T) { 201 // Each test case uses a new server 202 here, err := os.Getwd() 203 if err != nil { 204 t.Fatalf("error getting working directory: %s", err.Error()) 205 } 206 s := Server{ 207 cmd: "pdflatex", 208 errLog: log.New(log.Writer(), tc.Name+" Error: ", log.LstdFlags), 209 infoLog: log.New(ioutil.Discard, "", log.LstdFlags), 210 rootDir: here, 211 } 212 213 s.tmplCache, err = job.NewTemplateCache(1) 214 if err != nil { 215 t.Fatalf("error while creating template cache: %s", err.Error()) 216 } 217 // Does the test case require a local directory? 218 testDir, err := ioutil.TempDir("./", "test_"+tc.Name) 219 if err != nil { 220 t.Fatalf("error while creating temporary directory: %s", err.Error()) 221 } 222 s.rootDir = filepath.Join(s.rootDir, testDir) 223 os.Chdir(s.rootDir) 224 defer func() { 225 os.Chdir("../") 226 }() 227 // Does the test case require a mock db? 228 if tc.TexFileRegLevel == 2 || 229 tc.DtlsFileRegLevel == 2 || 230 tc.ResourcesRegLevel == 2 { 231 s.db = &mockDB{map[string]interface{}{}} 232 } 233 234 // Build up the url query and payload 235 q := url.Values{} 236 reqBody := struct { 237 Template string `json:"template"` 238 Details map[string]interface{} `json:"details"` 239 Resources map[string]string `json:"resources"` 240 Delimiters map[string]string `json:"delimiters, omitempty"` 241 OnMissingKey string `json:"onMissingKey, omitempty"` 242 Count uint `json:"count, omitempty"` 243 Compiler string `json:"compiler, omitempty"` 244 }{ 245 Delimiters: tc.Delimiters, 246 OnMissingKey: tc.OnMissingKey, 247 Count: tc.Count, 248 Compiler: tc.Compiler, 249 } 250 251 // Handle Tex file 252 path := "../../assets/templates/" + tc.TexFile 253 fileContentsBase64, err := GetContentsBase64(path) 254 if err != nil { 255 wd, _ := os.Getwd() 256 t.Fatalf("error while opening template file: %+v; wd: %s", err, wd) 257 } 258 switch tc.TexFileRegLevel { 259 case 0: 260 reqBody.Template = fileContentsBase64 261 case 1: 262 fileContents, err := ioutil.ReadFile(path) 263 if err != nil { 264 t.Fatalf("error while opening details file: %+v", err) 265 } 266 fPath := filepath.Join(s.rootDir, tc.TexFile) 267 err = toDisk(fileContents, fPath) 268 if err != nil { 269 wd, _ := os.Getwd() 270 t.Fatalf("error while writing file to disk: %s; wd: %s", err.Error(), wd) 271 } 272 q.Set("tmpl", tc.TexFile) 273 case 2: 274 fileContents, err := ioutil.ReadFile(path) 275 if err != nil { 276 t.Fatalf("error while opening details file: %+v", err) 277 } 278 err = s.db.Store(context.Background(), tc.TexFile, fileContents) 279 if err != nil { 280 t.Fatalf("error while saving file to db: %s", err.Error()) 281 } 282 q.Set("tmpl", tc.TexFile) 283 default: 284 t.Fatalf("invalid TexFileRegLevel value") 285 } 286 287 // Handle Dtls file 288 path = "../../assets/details/" + tc.DtlsFile 289 fileContentsJSON, err := GetContentsJSON(path) 290 if err != nil { 291 t.Fatalf("error while opening template file: %+v", err) 292 } 293 switch tc.DtlsFileRegLevel { 294 case 0: 295 reqBody.Details = fileContentsJSON 296 case 1: 297 fileContents, err := ioutil.ReadFile(path) 298 if err != nil { 299 t.Fatalf("error while opening details file: %+v", err) 300 } 301 fPath := tc.DtlsFile 302 err = toDisk(fileContents, fPath) 303 if err != nil { 304 t.Fatalf("error while writing file to disk: %s", err.Error()) 305 } 306 q.Set("dtls", tc.DtlsFile) 307 case 2: 308 fileContents, err := ioutil.ReadFile(path) 309 if err != nil { 310 t.Fatalf("error while opening details file: %+v", err) 311 } 312 err = s.db.Store(context.Background(), tc.DtlsFile, fileContents) 313 if err != nil { 314 t.Fatalf("error while saving file to db: %s", err.Error()) 315 } 316 q.Set("dtls", tc.DtlsFile) 317 default: 318 t.Fatalf("invalid DtlsRegLevel value") 319 } 320 321 // Handle Resource files 322 switch tc.ResourcesRegLevel { 323 case 0: 324 resources := make(map[string]string) 325 for _, rn := range tc.Resources { 326 path := "../../assets/resources/" + rn 327 resource, err := GetContentsBase64(path) 328 if err != nil { 329 t.Fatalf("error while opening resource file: %+v", err) 330 } 331 resources[rn] = resource 332 } 333 reqBody.Resources = resources 334 case 1: 335 for _, fileName := range tc.Resources { 336 path = "../../assets/resources/" + fileName 337 fileContents, err := ioutil.ReadFile(path) 338 if err != nil { 339 t.Fatalf("error while opening details file: %+v", err) 340 } 341 err = toDisk(fileContents, fileName) 342 if err != nil { 343 t.Fatalf("error while writing file to disk: %s", err.Error()) 344 } 345 q.Set("rsc", fileName) 346 } 347 case 2: 348 for _, fileName := range tc.Resources { 349 path = "../../assets/resources/" + fileName 350 fileContents, err := ioutil.ReadFile(path) 351 if err != nil { 352 t.Fatalf("error while opening details file: %+v", err) 353 } 354 err = s.db.Store(context.Background(), fileName, fileContents) 355 if err != nil { 356 t.Fatalf("error while saving file to mock db: %s", err.Error()) 357 } 358 q.Set("rsc", fileName) 359 } 360 361 default: 362 t.Fatalf("invalid ResourcesRegLevel value") 363 } 364 365 // Create request and ResponseWriter recorded 366 testPayload, err := json.Marshal(reqBody) 367 if err != nil { 368 t.Fatalf("error while creating request payload: %+v", err) 369 } 370 req := httptest.NewRequest("GET", "/generate", bytes.NewBuffer(testPayload)) 371 req.Header.Set("Content-Type", "application/json") 372 req.URL.RawQuery = q.Encode() 373 rr := httptest.NewRecorder() 374 375 // Create the HTTP handler to be tested and save current working directory to move back into 376 // after handler being tested is called; this is necessary since the handler changes the current working directory. 377 wd, err := os.Getwd() 378 if err != nil { 379 t.Fatalf("error while grabbing current directory: %+v", err) 380 } 381 os.Chdir("../") 382 s.handleGenerate()(rr, req) 383 err = os.Chdir(wd) 384 if err != nil { 385 t.Fatalf("error while moving back into testing directory") 386 } 387 response := rr.Result() 388 if response.StatusCode != 200 && tc.ExpectedToPass { 389 responseBody, err := ioutil.ReadAll(response.Body) 390 response.Body.Close() 391 if err != nil { 392 t.Fatalf("unable to read response body") 393 } 394 t.Fatalf(`Got non 200 status from result: {"status": %q, "response_body": %q}`, response.Status, string(responseBody)) 395 } 396 397 // If test case is expected to pass, grab expected PDF to test against and compare it to the received PDF 398 if tc.ExpectedToPass { 399 path := "../../assets/PDFs/" + tc.Expectation 400 expectedPDF, err := GetContentsBase64(path) 401 if err != nil { 402 t.Fatalf("error while reading expected PDF: %+v", err) 403 } 404 receivedPDF, err := ioutil.ReadAll(response.Body) 405 if err != nil { 406 t.Fatalf("error while reading received PDF: %+v", err) 407 } 408 response.Body.Close() 409 receivedPDF64 := base64.StdEncoding.EncodeToString(receivedPDF) 410 411 // Since PDFs seem to have some 'wiggle' to them, we have to make do with checking if our PDFs are 'close enough' 412 // (We define 'close enough' as no more than 1% difference when comparing byte-by-byte) 413 errorRate := DiffP(receivedPDF64, expectedPDF, t) 414 if errorRate > 1.0 { 415 t.Errorf("mismatch between received pdf and expected pdf exceeded 1%%: %f%%", errorRate) 416 } 417 } else if response.StatusCode == 200 { 418 t.Errorf("expected non 200 status code\n") 419 } 420 }) 421 } 422 } 423 424 func GetContentsBase64(path string) (string, error) { 425 f, err := os.Open(path) 426 defer f.Close() 427 if err != nil { 428 return "", err 429 } 430 fbytes, err := ioutil.ReadAll(f) 431 if err != nil { 432 return "", err 433 } 434 estring := base64.StdEncoding.EncodeToString(fbytes) 435 return estring, nil 436 } 437 438 func GetContentsJSON(path string) (map[string]interface{}, error) { 439 f, err := os.Open(path) 440 defer f.Close() 441 if err != nil { 442 return nil, err 443 } 444 data := make(map[string]interface{}) 445 err = json.NewDecoder(f).Decode(&data) 446 return data, err 447 } 448 449 // DiffP tests the equality of the two strings and returns the percentage by which they differ. 450 func DiffP(received, expected string, t *testing.T) float32 { 451 abs := len(received) - len(expected) 452 if abs < 0 { 453 abs = -1 * abs 454 } 455 if abs > 10 { 456 t.Fatalf("Received PDF differs from expected PDF: received length = %d \t expected length = %d", 457 len(received), len(expected)) 458 } 459 var mismatches int 460 for i, c := range received { 461 if len(expected) <= i { 462 break 463 } 464 if byte(c) != byte(expected[i]) { 465 mismatches++ 466 } 467 } 468 errorRate := float32(mismatches) / float32(len(expected)) 469 errorRate *= 100 470 return errorRate 471 }