package transport_test import ( "bytes" "context" "database/sql" "encoding/json" "errors" "log" "net/http" "net/http/httptest" "strings" "testing" "time" "github.com/alexedwards/flow" "github.com/golang/mock/gomock" "whitelistmanager/invite" mock_invite "whitelistmanager/mocks/invite" mock_minecraft "whitelistmanager/mocks/minecraft" mock_store "whitelistmanager/mocks/store" "whitelistmanager/store" "whitelistmanager/transport" ) type invitePayload struct { Server string `json:"server"` Unlimited bool `json:"unlimited"` Uses int `json:"uses"` } func TestInvites(t *testing.T) { ctrl := gomock.NewController(t) st := mock_store.NewMockStorer(ctrl) im := mock_invite.NewMockInviteManager(ctrl) mc := mock_minecraft.NewMockMinecraft(ctrl) user := store.User{ Id: "1", Token: "", DisplayName: "user", RefreshToken: "", TokenExpiry: time.Time{}, } server := store.Server{ Id: "1", Address: "foo", Owner: user, Rcon: store.Rcon{ Address: "foo", Password: "bar", }, } inv := store.Invite{ Token: "foo", Server: server, Creator: user, Uses: 0, Unlimited: false, } invNoToken := store.Invite{ Token: "", Server: store.Server{ Id: "1", }, Uses: 0, Unlimited: false, } invLog := store.InviteLog{ EntryID: "f", Invite: store.Invite{Token: inv.Token}, User: store.User{Id: user.Id}, } invPayload := invitePayload{ Server: "1", Uses: 0, Unlimited: false, } t.Run("create invite correctly", func(t *testing.T) { im.EXPECT().Create(invNoToken, user).Return(inv.Token, nil) handler := transport.New(st, im, mc) jsonData, err := json.Marshal(invPayload) if err != nil { log.Fatal(err) } req, err := http.NewRequest("POST", "/api/v1/invites", bytes.NewBuffer(jsonData)) if err != nil { t.Fatal(err) } ctx := req.Context() ctx = context.WithValue(ctx, "user", user) req = req.WithContext(ctx) rr := httptest.NewRecorder() h := http.HandlerFunc(handler.CreateInvite) h.ServeHTTP(rr, req) // Check the status code is what we expect. if status := rr.Code; status != http.StatusOK { t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusOK) } expected := `foo` if rr.Body.String() != expected { t.Errorf("handler returned unexpected body: got %v want %v", rr.Body.String(), expected) } }) t.Run("user is not server owner", func(t *testing.T) { im.EXPECT().Create(invNoToken, user).Return("", errors.New(invite.NotOwnerofServer)) handler := transport.New(st, im, mc) jsonData, err := json.Marshal(invPayload) if err != nil { log.Fatal(err) } req, err := http.NewRequest("POST", "/api/v1/invites", bytes.NewBuffer(jsonData)) if err != nil { t.Fatal(err) } ctx := req.Context() ctx = context.WithValue(ctx, "user", user) req = req.WithContext(ctx) rr := httptest.NewRecorder() h := http.HandlerFunc(handler.CreateInvite) h.ServeHTTP(rr, req) // Check the status code is what we expect. if status := rr.Code; status != http.StatusForbidden { t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusForbidden) } expected := "user is not owner of server\n" if rr.Body.String() != expected { t.Errorf("handler returned unexpected body: got %v want %v", rr.Body.String(), expected) } }) t.Run("get existing invite", func(t *testing.T) { m := flow.New() st.EXPECT().GetInvite(inv.Token).Return(inv, nil) handler := transport.New(st, im, mc) req, err := http.NewRequest("GET", "/api/v1/invite/"+inv.Token, nil) if err != nil { t.Fatal(err) } ctx := req.Context() ctx = context.WithValue(ctx, "user", user) req = req.WithContext(ctx) rr := httptest.NewRecorder() m.HandleFunc("/api/v1/invite/:id", handler.GetInvite) m.ServeHTTP(rr, req) // Check the status code is what we expect. if status := rr.Code; status != http.StatusOK { t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusOK) } expected, err := json.Marshal(inv) if err != nil { t.Fatal(err) } if rr.Body.String() != string(expected)+"\n" { t.Errorf("handler returned unexpected body: got %v want %v", rr.Body.String(), string(expected)) } }) t.Run("existing invite", func(t *testing.T) { m := flow.New() st.EXPECT().GetInvite(inv.Token).Return(inv, nil) handler := transport.New(st, im, mc) req, err := http.NewRequest("GET", "/api/v1/invite/"+inv.Token, nil) if err != nil { t.Fatal(err) } ctx := req.Context() ctx = context.WithValue(ctx, "user", user) req = req.WithContext(ctx) rr := httptest.NewRecorder() m.HandleFunc("/api/v1/invite/:id", handler.GetInvite) m.ServeHTTP(rr, req) // Check the status code is what we expect. if status := rr.Code; status != http.StatusOK { t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusOK) } expected, err := json.Marshal(inv) if err != nil { t.Fatal(err) } if rr.Body.String() != string(expected)+"\n" { t.Errorf("handler returned unexpected body: got %v want %v", rr.Body.String(), string(expected)) } }) t.Run("non existent invite", func(t *testing.T) { m := flow.New() st.EXPECT().GetInvite(inv.Token).Return(store.Invite{}, sql.ErrNoRows) handler := transport.New(st, im, mc) req, err := http.NewRequest("GET", "/api/v1/invite/"+inv.Token, nil) if err != nil { t.Fatal(err) } ctx := req.Context() ctx = context.WithValue(ctx, "user", user) req = req.WithContext(ctx) rr := httptest.NewRecorder() m.HandleFunc("/api/v1/invite/:id", handler.GetInvite) m.ServeHTTP(rr, req) if status := rr.Code; status != http.StatusNotFound { t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusNotFound) } }) t.Run("user not logged in when getting invite", func(t *testing.T) { m := flow.New() st.EXPECT().GetInvite(inv.Token).Return(inv, nil) handler := transport.New(st, im, mc) req, err := http.NewRequest("GET", "/api/v1/invite/"+inv.Token, nil) if err != nil { t.Fatal(err) } rr := httptest.NewRecorder() m.HandleFunc("/api/v1/invite/:id", handler.GetInvite) m.ServeHTTP(rr, req) if status := rr.Code; status != http.StatusOK { t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusNotFound) } expected, err := json.Marshal(inv) if err != nil { t.Fatal(err) } if rr.Body.String() != string(expected)+"\n" { t.Errorf("handler returned unexpected body: got %v want %v", rr.Body.String(), string(expected)) } }) t.Run("user can accept invite", func(t *testing.T) { m := flow.New() st.EXPECT().GetInvite(inv.Token).Return(inv, nil) im.EXPECT().RemainingUses(inv).Return(1, nil) st.EXPECT().GetServer(server.Id).Return(server, nil) mc.EXPECT().Whitelist(user.DisplayName, server).Return("success", nil) handler := transport.New(st, im, mc) req, err := http.NewRequest("POST", "/api/v1/invite/"+inv.Token+"/accept", nil) if err != nil { t.Fatal(err) } ctx := req.Context() ctx = context.WithValue(ctx, "user", user) req = req.WithContext(ctx) rr := httptest.NewRecorder() m.HandleFunc("/api/v1/invite/:id/accept", handler.AcceptInvite, "POST") m.ServeHTTP(rr, req) if status := rr.Code; status != http.StatusOK { t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusOK) } expected := "success" if rr.Body.String() != expected { t.Errorf("handler returned unexpected body: got %v want %v", rr.Body.String(), expected) } }) t.Run("user can't accept unknown invite", func(t *testing.T) { m := flow.New() st.EXPECT().GetInvite(inv.Token).Return(store.Invite{}, sql.ErrNoRows) handler := transport.New(st, im, mc) req, err := http.NewRequest("POST", "/api/v1/invite/"+inv.Token+"/accept", nil) if err != nil { t.Fatal(err) } ctx := req.Context() ctx = context.WithValue(ctx, "user", user) req = req.WithContext(ctx) rr := httptest.NewRecorder() m.HandleFunc("/api/v1/invite/:id/accept", handler.AcceptInvite, "POST") m.ServeHTTP(rr, req) if status := rr.Code; status != http.StatusNotFound { t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusNotFound) } }) t.Run("user can't accept invite with no remaining uses", func(t *testing.T) { m := flow.New() st.EXPECT().GetInvite(inv.Token).Return(inv, nil) im.EXPECT().RemainingUses(inv).Return(0, nil) handler := transport.New(st, im, mc) req, err := http.NewRequest("POST", "/api/v1/invite/"+inv.Token+"/accept", nil) if err != nil { t.Fatal(err) } ctx := req.Context() ctx = context.WithValue(ctx, "user", user) req = req.WithContext(ctx) rr := httptest.NewRecorder() m.HandleFunc("/api/v1/invite/:id/accept", handler.AcceptInvite, "POST") m.ServeHTTP(rr, req) if status := rr.Code; status != http.StatusForbidden { t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusForbidden) } }) t.Run("invite log returns logs", func(t *testing.T) { m := flow.New() st.EXPECT().GetInvite(inv.Token).Return(inv, nil) st.EXPECT().InviteLog(inv).Return([]store.InviteLog{invLog}, nil) handler := transport.New(st, im, mc) req, err := http.NewRequest("GET", "/api/v1/invite/"+inv.Token+"/log", nil) if err != nil { t.Fatal(err) } ctx := req.Context() ctx = context.WithValue(ctx, "user", user) req = req.WithContext(ctx) rr := httptest.NewRecorder() m.HandleFunc("/api/v1/invite/:id/log", handler.InviteLog) m.ServeHTTP(rr, req) // Check the status code is what we expect. if status := rr.Code; status != http.StatusOK { t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusOK) } var expected []store.InviteLog err = json.Unmarshal(rr.Body.Bytes(), &expected) if err != nil { t.Fatal(err) } if expected[0].EntryID != invLog.EntryID { t.Errorf("handler returned unexpected entry id: got %v want %v", rr.Body.String(), invLog.EntryID) } }) t.Run("invite log returns no logs", func(t *testing.T) { m := flow.New() st.EXPECT().GetInvite(inv.Token).Return(inv, nil) st.EXPECT().InviteLog(inv).Return([]store.InviteLog{}, nil) handler := transport.New(st, im, mc) req, err := http.NewRequest("GET", "/api/v1/invite/"+inv.Token+"/log", nil) if err != nil { t.Fatal(err) } ctx := req.Context() ctx = context.WithValue(ctx, "user", user) req = req.WithContext(ctx) rr := httptest.NewRecorder() m.HandleFunc("/api/v1/invite/:id/log", handler.InviteLog) m.ServeHTTP(rr, req) // Check the status code is what we expect. if status := rr.Code; status != http.StatusNotFound { t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusNotFound) } expected := "[]" if rr.Body.String() != expected { t.Errorf("handler returned unexpected body: got %v want %v", rr.Body.String(), expected) } }) t.Run("delete an invite", func(t *testing.T) { st.EXPECT().GetInvite(inv.Token).Return(inv, nil) st.EXPECT().DeleteInvite(inv).Return(nil) jsonData, err := json.Marshal(server) if err != nil { log.Fatal(err) } m := flow.New() handler := transport.New(st, im, mc) req, err := http.NewRequest("DELETE", "/api/v1/invite/"+inv.Token, bytes.NewBuffer(jsonData)) if err != nil { t.Fatal(err) } ctx := req.Context() ctx = context.WithValue(ctx, "user", user) req = req.WithContext(ctx) rr := httptest.NewRecorder() m.HandleFunc("/api/v1/invite/:id", handler.DeleteInvite) m.ServeHTTP(rr, req) if status := rr.Code; status != http.StatusOK { t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusNotFound) } expected := "deleted" if rr.Body.String() != expected { t.Errorf("handler returned unexpected body: got %v want %v", rr.Body.String(), expected) } }) } func TestUser(t *testing.T) { ctrl := gomock.NewController(t) st := mock_store.NewMockStorer(ctrl) im := mock_invite.NewMockInviteManager(ctrl) mc := mock_minecraft.NewMockMinecraft(ctrl) user := store.User{ Id: "1", Token: "", DisplayName: "user", RefreshToken: "", TokenExpiry: time.Time{}, } t.Run("return username", func(t *testing.T) { m := flow.New() handler := transport.New(st, im, mc) req, err := http.NewRequest("GET", "/api/v1/me", nil) if err != nil { t.Fatal(err) } ctx := req.Context() ctx = context.WithValue(ctx, "user", user) req = req.WithContext(ctx) rr := httptest.NewRecorder() m.HandleFunc("/api/v1/me", handler.CurrentUser) m.ServeHTTP(rr, req) if status := rr.Code; status != http.StatusOK { t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusOK) } if rr.Body.String() != user.DisplayName { t.Errorf("handler returned unexpected body: got %v want %v", rr.Body.String(), user.DisplayName) } }) } func TestServers(t *testing.T) { ctrl := gomock.NewController(t) st := mock_store.NewMockStorer(ctrl) im := mock_invite.NewMockInviteManager(ctrl) mc := mock_minecraft.NewMockMinecraft(ctrl) user := store.User{ Id: "1", Token: "", DisplayName: "user", RefreshToken: "", TokenExpiry: time.Time{}, } server := store.Server{ Id: "1", Owner: user, } inv := store.Invite{ Token: "foo", Server: server, Uses: 0, Unlimited: false, } t.Run("fetch a user's servers", func(t *testing.T) { st.EXPECT().GetUserServers(user).Return([]store.Server{server}, nil) m := flow.New() handler := transport.New(st, im, mc) req, err := http.NewRequest("GET", "/api/v1/servers", nil) if err != nil { t.Fatal(err) } ctx := req.Context() ctx = context.WithValue(ctx, "user", user) req = req.WithContext(ctx) rr := httptest.NewRecorder() m.HandleFunc("/api/v1/servers", handler.Servers) m.ServeHTTP(rr, req) if status := rr.Code; status != http.StatusOK { t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusOK) } expected := "[{\"id\":\"1\",\"name\":\"\",\"address\":\"\",\"rcon\":{}}]\n" if rr.Body.String() != expected { t.Errorf("handler returned unexpected body: got %v want %v", rr.Body.String(), expected) } }) t.Run("fetch an inividual server", func(t *testing.T) { st.EXPECT().GetServer(server.Id).Return(server, nil) m := flow.New() handler := transport.New(st, im, mc) req, err := http.NewRequest("GET", "/api/v1/server/1", nil) if err != nil { t.Fatal(err) } ctx := req.Context() ctx = context.WithValue(ctx, "user", user) req = req.WithContext(ctx) rr := httptest.NewRecorder() m.HandleFunc("/api/v1/server/:id", handler.Server) m.ServeHTTP(rr, req) if status := rr.Code; status != http.StatusOK { t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusOK) } var body store.Server err = json.Unmarshal(rr.Body.Bytes(), &body) if err != nil { t.Fatal(err) } if body.Rcon.Password != "" { t.Errorf("handler returned rcon password: got %v want %v", body.Rcon.Password, "\"\"") } if body.Address != server.Address { t.Errorf("handler returned wrong address: got %v want %v", body.Address, server.Address) } }) t.Run("create a server for a user", func(t *testing.T) { st.EXPECT().SaveServer(gomock.Any()).Return(nil) jsonData, err := json.Marshal(server) if err != nil { log.Fatal(err) } m := flow.New() handler := transport.New(st, im, mc) req, err := http.NewRequest("POST", "/api/v1/servers", bytes.NewBuffer(jsonData)) if err != nil { t.Fatal(err) } ctx := req.Context() ctx = context.WithValue(ctx, "user", user) req = req.WithContext(ctx) rr := httptest.NewRecorder() m.HandleFunc("/api/v1/servers", handler.Servers) m.ServeHTTP(rr, req) if status := rr.Code; status != http.StatusOK { t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusOK) } if !strings.Contains(rr.Body.String(), "created server") { t.Errorf("handler returned unexpected body: got %v want %v", rr.Body.String(), "created server") } }) t.Run("delete a server for a user", func(t *testing.T) { st.EXPECT().GetServer(server.Id).Return(server, nil) st.EXPECT().DeleteServer(server, user).Return(nil) jsonData, err := json.Marshal(server) if err != nil { log.Fatal(err) } m := flow.New() handler := transport.New(st, im, mc) req, err := http.NewRequest("DELETE", "/api/v1/server/1", bytes.NewBuffer(jsonData)) if err != nil { t.Fatal(err) } ctx := req.Context() ctx = context.WithValue(ctx, "user", user) req = req.WithContext(ctx) rr := httptest.NewRecorder() m.HandleFunc("/api/v1/server/:id", handler.Server, "DELETE") m.ServeHTTP(rr, req) if status := rr.Code; status != http.StatusOK { t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusOK) } if rr.Body.String() != "deleted" { t.Errorf("handler returned unexpected body: got %v want %v", rr.Body.String(), "created server") } }) t.Run("get invite list for server", func(t *testing.T) { st.EXPECT().GetServer(server.Id).Return(server, nil) st.EXPECT().ServerInvites(server).Return([]store.Invite{inv}, nil) jsonData, err := json.Marshal(server) if err != nil { log.Fatal(err) } m := flow.New() handler := transport.New(st, im, mc) req, err := http.NewRequest("POST", "/api/v1/server/1", bytes.NewBuffer(jsonData)) if err != nil { t.Fatal(err) } ctx := req.Context() ctx = context.WithValue(ctx, "user", user) req = req.WithContext(ctx) rr := httptest.NewRecorder() m.HandleFunc("/api/v1/server/:id", handler.ServerInvites) m.ServeHTTP(rr, req) if status := rr.Code; status != http.StatusOK { t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusOK) } expected := "[{\"token\":\"foo\",\"creator\":{\"display_name\":\"\"},\"server\":{\"id\":\"1\",\"name\":\"\",\"address\":\"\",\"rcon\":{}},\"uses\":0,\"unlimited\":false}]\n" if rr.Body.String() != expected { t.Errorf("handler returned unexpected body: got %v want %v", rr.Body.String(), expected) } }) t.Run("get empty invite list for server", func(t *testing.T) { st.EXPECT().GetServer(server.Id).Return(server, nil) st.EXPECT().ServerInvites(server).Return([]store.Invite{}, nil) jsonData, err := json.Marshal(server) if err != nil { log.Fatal(err) } m := flow.New() handler := transport.New(st, im, mc) req, err := http.NewRequest("POST", "/api/v1/server/1/invites", bytes.NewBuffer(jsonData)) if err != nil { t.Fatal(err) } ctx := req.Context() ctx = context.WithValue(ctx, "user", user) req = req.WithContext(ctx) rr := httptest.NewRecorder() m.HandleFunc("/api/v1/server/:id/invites", handler.ServerInvites) m.ServeHTTP(rr, req) if status := rr.Code; status != http.StatusNotFound { t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusNotFound) } expected := "[]" if rr.Body.String() != expected { t.Errorf("handler returned unexpected body: got %v want %v", rr.Body.String(), expected) } }) } func TestMiddlewares(t *testing.T) { ctrl := gomock.NewController(t) st := mock_store.NewMockStorer(ctrl) im := mock_invite.NewMockInviteManager(ctrl) mc := mock_minecraft.NewMockMinecraft(ctrl) user := store.User{ Id: "1", Token: "", DisplayName: "user", RefreshToken: "", TokenExpiry: time.Time{}, } sess := store.Session{ Token: "foo", UID: user.Id, Expiry: time.Time{}, } t.Run("cors middleware", func(t *testing.T) { m := flow.New() handler := transport.New(st, im, mc) m.Use(handler.Cors) req, err := http.NewRequest("GET", "/", nil) if err != nil { t.Fatal(err) } rr := httptest.NewRecorder() m.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { r.Header.Set("Content-Type", "application/json") w.Write([]byte(`{"duck": "quacks"}`)) }) m.ServeHTTP(rr, req) if status := rr.Code; status != http.StatusOK { t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusOK) } if rr.Header().Get("Access-Control-Allow-Origin") != "*" { t.Errorf("handler returned unexpected Access-Control-Allow-Origin header: got %v want %v", rr.Header().Get("Access-Control-Allow-Origin"), "*") } }) t.Run("auth success with valid session token", func(t *testing.T) { st.EXPECT().SessionUser(sess.Token).Return(user, nil) m := flow.New() handler := transport.New(st, im, mc) m.Use(handler.SessionAuth) req, err := http.NewRequest("GET", "/", nil) if err != nil { t.Fatal(err) } req.AddCookie(&http.Cookie{ Name: "session", Value: "foo", }) rr := httptest.NewRecorder() m.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { r.Header.Set("Content-Type", "application/json") w.Write([]byte(`{"duck": "quacks"}`)) }) m.ServeHTTP(rr, req) if status := rr.Code; status != http.StatusOK { t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusOK) } expected := `{"duck": "quacks"}` if rr.Body.String() != expected { t.Errorf("handler returned unexpected body: got %v want %v", rr.Body.String(), expected) } }) t.Run("auth denial with no session cookie", func(t *testing.T) { m := flow.New() handler := transport.New(st, im, mc) m.Use(handler.SessionAuth) req, err := http.NewRequest("GET", "/", nil) if err != nil { t.Fatal(err) } rr := httptest.NewRecorder() m.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { r.Header.Set("Content-Type", "application/json") w.Write([]byte(`{"duck": "quacks"}`)) }) m.ServeHTTP(rr, req) if status := rr.Code; status != http.StatusForbidden { t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusForbidden) } }) t.Run("auth denial with invalid session token", func(t *testing.T) { st.EXPECT().SessionUser(sess.Token).Return(store.User{}, sql.ErrNoRows) m := flow.New() handler := transport.New(st, im, mc) m.Use(handler.SessionAuth) req, err := http.NewRequest("GET", "/", nil) if err != nil { t.Fatal(err) } req.AddCookie(&http.Cookie{ Name: "session", Value: "foo", }) rr := httptest.NewRecorder() m.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { r.Header.Set("Content-Type", "application/json") w.Write([]byte(`{"duck": "quacks"}`)) }) m.ServeHTTP(rr, req) if status := rr.Code; status != http.StatusForbidden { t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusForbidden) } }) }