From a2c8e2b054fb86d7d4a907389e297fc1d8a3b972 Mon Sep 17 00:00:00 2001 From: T T Date: Tue, 6 Nov 2018 07:16:03 +0100 Subject: [PATCH] Initial commit --- go.mod | 3 + go.sum | 2 + main.go | 130 ++++++++++++++++++++++++++++++++++++++++++ main_test.go | 158 +++++++++++++++++++++++++++++++++++++++++++++++++++ templates.go | 22 +++++++ 5 files changed, 315 insertions(+) create mode 100644 go.mod create mode 100644 go.sum create mode 100644 main.go create mode 100644 main_test.go create mode 100644 templates.go diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..156e9dd --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module github.com/you/hello + +require github.com/satori/go.uuid v1.2.0 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..a9a7620 --- /dev/null +++ b/go.sum @@ -0,0 +1,2 @@ +github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww= +github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= diff --git a/main.go b/main.go new file mode 100644 index 0000000..d1985c4 --- /dev/null +++ b/main.go @@ -0,0 +1,130 @@ +package main + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "log" + "net/http" + "regexp" + + "github.com/satori/go.uuid" +) + +const ( + FormUrl = "/" + FormPostUrl = "/post" + RequestStatusUrlPrefix = "/status/" + ErrorUrl = "/error" +) + +var statusRE = regexp.MustCompile(RequestStatusUrlPrefix + `(.+)$`) + +type Id uuid.UUID + +func (id Id) String() string { + return uuid.UUID(id).String() +} + +type Request struct { + Username string + Email string + Why string + SSHPublicKey string + Status string +} + +func (r *Request) IsValid() bool { + return r.Username != "" && + r.Email != "" && + r.Why != "" && + r.SSHPublicKey != "" +} + +type Io interface { + Save(r Request) (Id, error) + Load(id Id) (*Request, error) +} + +type FsIo struct { +} + +func (io *FsIo) Save(r Request) (Id, error) { + b, err := json.MarshalIndent(r, "", "\t") + if err != nil { + return Id{}, err + } + id := Id(uuid.NewV4()) + return id, ioutil.WriteFile(id.String()+".json", b, 0600) +} + +func (io *FsIo) Load(id Id) (*Request, error) { + b, err := ioutil.ReadFile(id.String() + ".json") + if err != nil { + return nil, err + } + var req Request + if err := json.Unmarshal(b, &req); err != nil { + return nil, err + } + return &req, nil +} + +type Server struct { + Io Io +} + +func (s *Server) RequestPage(w http.ResponseWriter, r *http.Request) { + m := statusRE.FindStringSubmatch(r.URL.String()) + if len(m) != 2 { + http.Error(w, "missing request id", http.StatusBadRequest) + return + } + uid := m[1] + id, err := uuid.FromString(uid) + if err != nil { + http.Error(w, "no such request: '"+uid+"'", http.StatusBadRequest) + return + } + req, err := s.Io.Load(Id(id)) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + fmt.Fprintf(w, "Status: %v", req.Status) +} + +func (s *Server) IncorrectRequest(w http.ResponseWriter, r *http.Request) { +} + +func (s *Server) FormPostHandler(w http.ResponseWriter, r *http.Request) { + r.ParseForm() + req := Request{} + req.Username = r.PostFormValue("username") + req.Email = r.PostFormValue("email") + req.Why = r.PostFormValue("why") + req.SSHPublicKey = r.PostFormValue("sshpublickey") + req.Status = "Pending" + if !req.IsValid() { + log.Println("Invalid request", r.PostForm) + http.Redirect(w, r, ErrorUrl, http.StatusSeeOther) + return + } + id, err := s.Io.Save(req) + log.Println("Valid request", r.PostForm, err) + http.Redirect(w, r, RequestStatusUrlPrefix+id.String(), http.StatusSeeOther) +} + +func (s *Server) FormPage(w http.ResponseWriter, r *http.Request) { + formTemplate.Execute(w, nil) +} + +func main() { + var io FsIo + server := Server{Io: &io} + http.HandleFunc(RequestStatusUrlPrefix, server.RequestPage) + http.HandleFunc(ErrorUrl, server.IncorrectRequest) + http.HandleFunc(FormPostUrl, server.FormPostHandler) + http.HandleFunc(FormUrl, server.FormPage) + log.Fatal(http.ListenAndServe(":8080", nil)) +} diff --git a/main_test.go b/main_test.go new file mode 100644 index 0000000..c934be1 --- /dev/null +++ b/main_test.go @@ -0,0 +1,158 @@ +package main + +import ( + "fmt" + "net/http" + "net/http/httptest" + "net/url" + "reflect" + "testing" + + uuid "github.com/satori/go.uuid" +) + +var expected = Request{ + Username: "name", + Email: "test@example.com", + Why: "foo bar baz", + SSHPublicKey: "123", + Status: "Pending", +} + +type ReqEntry struct { + Request Request + Id Id +} + +type IoStub struct { + Saved []ReqEntry + Loads []Id +} + +func NewIoStub() *IoStub { + return &IoStub{} +} + +func (io *IoStub) Save(r Request) (Id, error) { + id := Id(uuid.NewV4()) + io.Saved = append(io.Saved, ReqEntry{r, id}) + return id, nil +} + +func (io *IoStub) Load(id Id) (*Request, error) { + io.Loads = append(io.Loads, id) + for _, r := range io.Saved { + if r.Id == id { + return &r.Request, nil + } + } + return nil, fmt.Errorf("Missing Request for id: %v", id) +} + +func requestForm(target string, values map[string]string) *http.Request { + r := httptest.NewRequest("POST", target, nil) + r.PostForm = url.Values{} + for k, v := range values { + r.PostForm.Set(k, v) + } + return r +} + +func TestRequestSaveAfterCorrectFormPost(t *testing.T) { + io := NewIoStub() + server := Server{Io: io} + req := requestForm(FormPostUrl, map[string]string{ + "username": expected.Username, + "email": expected.Email, + "why": expected.Why, + "sshpublickey": expected.SSHPublicKey, + }) + recorder := httptest.NewRecorder() + server.FormPostHandler(recorder, req) + resp := recorder.Result() + if resp.StatusCode != http.StatusSeeOther { + t.Fatalf("Expected status %v, got: %v", http.StatusSeeOther, resp.StatusCode) + } + expectedLoc := RequestStatusUrlPrefix + io.Saved[0].Id.String() + if loc := resp.Header.Get("Location"); loc != expectedLoc { + t.Fatalf("Expected location '%v', got '%v'", expectedLoc, loc) + } + if !reflect.DeepEqual(expected, io.Saved[0].Request) { + t.Fatalf("\nExpected '%#v'\n got '%#v'", expected, io.Saved[0]) + } +} + +func TestRedirectToFailureWhenAnyRequestFieldIsEmtpy(t *testing.T) { + data := []Request{ + {Username: "", Email: expected.Email, Why: expected.Why, SSHPublicKey: expected.SSHPublicKey}, + {Username: expected.Username, Email: "", Why: expected.Why, SSHPublicKey: expected.SSHPublicKey}, + {Username: expected.Username, Email: expected.Email, Why: "", SSHPublicKey: expected.SSHPublicKey}, + {Username: expected.Username, Email: expected.Email, Why: expected.Why, SSHPublicKey: ""}, + } + for _, r := range data { + io := NewIoStub() + server := Server{Io: io} + req := requestForm(FormPostUrl, map[string]string{ + "username": r.Username, + "email": r.Email, + "why": r.Why, + "sshpublickey": r.SSHPublicKey, + }) + recorder := httptest.NewRecorder() + server.FormPostHandler(recorder, req) + resp := recorder.Result() + if resp.StatusCode != http.StatusSeeOther { + t.Fatalf("Expected see other status, got: %v", resp.StatusCode) + } + if loc := resp.Header.Get("Location"); loc != ErrorUrl { + t.Fatalf("Expected location %v, got: %v", ErrorUrl, loc) + } + } +} + +func TestStatusPageOk(t *testing.T) { + io := NewIoStub() + server := Server{Io: io} + id, _ := io.Save(expected) + req := httptest.NewRequest("GET", RequestStatusUrlPrefix+id.String(), nil) + rec := httptest.NewRecorder() + server.RequestPage(rec, req) + if io.Loads[0] != id { + t.Fatalf("Expected load of %v, loaded %v instead", id, io.Loads[0]) + } +} + +func TestStatusPageUnknownId(t *testing.T) { + io := NewIoStub() + server := Server{Io: io} + id := Id(uuid.NewV4()) + req := httptest.NewRequest("GET", RequestStatusUrlPrefix+id.String(), nil) + rec := httptest.NewRecorder() + server.RequestPage(rec, req) + if io.Loads[0] != id { + t.Fatalf("Expected load of %v, loaded %v instead", id, io.Loads[0]) + } +} + +func TestStatusPageMalformedId(t *testing.T) { + io := NewIoStub() + server := Server{Io: io} + id := Id(uuid.NewV4()) + req := httptest.NewRequest("GET", RequestStatusUrlPrefix+id.String()+"abc", nil) + rec := httptest.NewRecorder() + server.RequestPage(rec, req) + if l := len(io.Loads); l != 0 { + t.Fatalf("Expected zero loads, got %v", l) + } +} + +func TestStatusPageMissingId(t *testing.T) { + io := NewIoStub() + server := Server{Io: io} + req := httptest.NewRequest("GET", RequestStatusUrlPrefix, nil) + rec := httptest.NewRecorder() + server.RequestPage(rec, req) + if l := len(io.Loads); l != 0 { + t.Fatalf("Expected zero loads, got %v", l) + } +} diff --git a/templates.go b/templates.go new file mode 100644 index 0000000..77b973f --- /dev/null +++ b/templates.go @@ -0,0 +1,22 @@ +package main + +import "html/template" + +var formTemplate = template.Must(template.New("form").Parse(` + + +

~🐱 signup form

+
+Username: +
+Email: +
+Why would you want an account here? +
+SSH key: +
+ +
+ + +`))