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  }