package api import ( "bytes" "encoding/json" "fmt" "io" "mime/multipart" "net/http" "net/http/httptest" "net/url" "os" "path/filepath" "strings" "testing" "time" "dns.froth.zone/pomme/internal" "dns.froth.zone/pomme/internal/db" "github.com/go-chi/chi/v5" "github.com/go-chi/jwtauth/v5" "github.com/stretchr/testify/assert" "golang.org/x/crypto/bcrypt" ) type response struct { Username string `json:"username"` Message string `json:"message"` Status int `json:"status"` } type accountTest struct { username string password string token string url string } func makeTestToken(username string) (tokenString string, err error) { claim := map[string]interface{}{"username": username, "admin": false} jwtauth.SetExpiry(claim, time.Now().Add(time.Minute)) if _, tokenString, err = tokenAuth.Encode(claim); err == nil { return } return "", fmt.Errorf("unable to generate JWT: %w", err) } func TestAPI(t *testing.T) { config, err := internal.ReadConfig() if err != nil { panic(err) } pomme := chi.NewRouter() pomme.Mount("/api", API()) s := &http.Server{ ReadTimeout: 3 * time.Second, WriteTimeout: 15 * time.Second, Addr: ":" + config.Port, Handler: pomme, } ts := httptest.NewUnstartedServer(pomme) ts.Config = s ts.Start() defer ts.Close() tester := Init(ts.URL) c, err := internal.ReadConfig() if err != nil { assert.NotNil(t, err) } db, err, ok := db.InitDb(c.TestDB) if err != nil && !ok { assert.NotNil(t, err) } hashedPassword, err := bcrypt.GenerateFromPassword([]byte(tester.password), bcrypt.DefaultCost) if err != nil { assert.NotNil(t, err) } db.Create(&internal.User{Username: tester.username, HashedPassword: string(hashedPassword)}) tester.TestMakeAccount(t) tester.TestLogin(t) tester.TestLogout(t) tester.TestUpload(t) } func Init(url string) accountTest { user := autoUname() token, err := makeTestToken(user) if err != nil { return accountTest{} } test := accountTest{ username: user, password: "merde", url: url, token: token, } return test } func (a *accountTest) TestMakeAccount(t *testing.T) { var target response client := http.Client{} form := url.Values{} form.Add("username", a.username) form.Add("password", a.password) if req, err := http.NewRequest(http.MethodPost, a.url+`api/create`, strings.NewReader(form.Encode())); err == nil { req.Header.Add("Content-Type", "application/x-www-form-urlencoded") req.Header.Add("User-Agent", "pomme-api-test-slave") resp, err := client.Do(req) if err != nil { assert.NotNil(t, err) } respBody, _ := io.ReadAll(resp.Body) err = json.Unmarshal(respBody, &target) if err != nil { assert.NotNil(t, err) } assert.Equal(t, http.StatusCreated, target.Status) } } func (a *accountTest) TestLogin(t *testing.T) { var target response client := http.Client{} form := url.Values{} form.Add("username", a.username) form.Add("password", a.password) if req, err := http.NewRequest(http.MethodPost, a.url+`/api/login`, strings.NewReader(form.Encode())); err == nil { req.Header.Add("Content-Type", "application/x-www-form-urlencoded") req.Header.Add("User-Agent", "pomme-api-test-slave") resp, err := client.Do(req) if err != nil { assert.NotNil(t, err) } respBody, _ := io.ReadAll(resp.Body) err = json.Unmarshal(respBody, &target) if err != nil { assert.NotNil(t, err) } assert.Equal(t, http.StatusOK, resp.StatusCode) } } func (a *accountTest) TestLogout(t *testing.T) { var target response client := http.Client{} form := url.Values{} form.Add("username", a.username) if req, err := http.NewRequest(http.MethodPost, a.url+`/api/logout`, strings.NewReader(form.Encode())); err == nil { req.Header.Add("Content-Type", "application/x-www-form-urlencoded") req.Header.Add("User-Agent", "pomme-api-test-slave") resp, err := client.Do(req) if err != nil { assert.NotNil(t, err) } respBody, _ := io.ReadAll(resp.Body) err = json.Unmarshal(respBody, &target) if err != nil { assert.NotNil(t, err) } assert.Equal(t, http.StatusOK, resp.StatusCode) } } type expectedValues struct { response int } func (a *accountTest) TestUpload(t *testing.T) { testCases := []struct { name string file string contentType string fileContents []byte expected expectedValues }{ { name: "Should fail to upload an empty file", file: "badzonefile", contentType: "audio/aac", expected: expectedValues{ response: http.StatusInternalServerError, }, }, { name: "Should upload a valid file", file: "zonefile", contentType: "multipart/form-data", fileContents: []byte{}, expected: expectedValues{ response: http.StatusCreated, }, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { var ( f *os.File target response err error buf = new(bytes.Buffer) w = multipart.NewWriter(buf) ) switch tc.name { case "Should fail to upload an empty file": f, err = os.CreateTemp(".", tc.file) if err != nil { assert.NotNil(t, err) } default: f, err = os.CreateTemp(".", "zonefile") if err != nil { assert.NotNil(t, err) } if err = os.WriteFile(f.Name(), zonebytes, 0o600); err != nil { assert.NotNil(t, err) } } defer os.Remove(f.Name()) //nolint: errcheck client := http.Client{} part, err := w.CreateFormFile("file", filepath.Base(f.Name())) if err != nil { assert.NotNil(t, err) } b, err := os.ReadFile(f.Name()) if err != nil { assert.NotNil(t, err) } _, err = part.Write(b) if err != nil { assert.NotNil(t, err) } err = w.Close() if err != nil { assert.NotNil(t, err) } if req, err := http.NewRequest(http.MethodPost, a.url+`/api/upload`, buf); err == nil { switch tc.name { case "Should fail to upload an empty file": req.Header.Add("Content-Type", tc.contentType) default: req.Header.Add("Content-Type", w.FormDataContentType()) } req.Header.Add("Authorization", `Bearer:`+a.token) req.Header.Add("User-Agent", "pomme-api-test-slave") resp, err := client.Do(req) if err != nil { assert.NotNil(t, err) } respBody, _ := io.ReadAll(resp.Body) err = json.Unmarshal(respBody, &target) if err != nil { assert.NotNil(t, err) } assert.Equal(t, tc.expected.response, resp.StatusCode) } }) } }