Skip to content

Commit

Permalink
[authentication] user.CreatedAt uses timestamp type to write to db co…
Browse files Browse the repository at this point in the history
…nsistently
  • Loading branch information
bhongy committed Jul 31, 2021
1 parent 6a5432d commit 9e3e4cb
Show file tree
Hide file tree
Showing 8 changed files with 91 additions and 15 deletions.
13 changes: 7 additions & 6 deletions authentication/api/user_signup.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import (
"errors"
"log"
"net/http"
"time"

"github.com/bhongy/kimidori/authentication/user"
)
Expand All @@ -20,9 +19,9 @@ type UserSignupRequest struct {
}

type UserSignupResponse struct {
ID string `json:"id"`
Username string `json:"username"`
CreatedAt time.Time `json:"createdAt"`
ID string `json:"id"`
Username string `json:"username"`
CreatedAt string `json:"createdAt"`
}

type userSignupHandler struct {
Expand All @@ -31,7 +30,9 @@ type userSignupHandler struct {

func (h *userSignupHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.NotFound(w, r)
w.Header().Set("Allow", "POST")
code := http.StatusMethodNotAllowed
http.Error(w, http.StatusText(code), code)
return
}

Expand Down Expand Up @@ -64,7 +65,7 @@ func (h *userSignupHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
body, err := json.Marshal(UserSignupResponse{
ID: u.ID,
Username: u.Username,
CreatedAt: u.CreatedAt,
CreatedAt: u.CreatedAt.String(),
})

if err != nil {
Expand Down
4 changes: 2 additions & 2 deletions authentication/api/user_signup_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ func TestUserSignup(t *testing.T) {
u := user.User{
ID: "fake.id",
Username: "fake.username",
CreatedAt: time.Now(),
CreatedAt: user.NewTimestamp(time.Now()),
}

body, err := json.Marshal(api.UserSignupRequest{
Expand Down Expand Up @@ -57,7 +57,7 @@ func TestUserSignup(t *testing.T) {
`{"id":"%v","username":"%v","createdAt":"%v"}`,
u.ID,
u.Username,
u.CreatedAt.Format(time.RFC3339Nano),
u.CreatedAt.String(),
)
if body := recorder.Body.String(); body != expectedBody {
t.Errorf("handler returned unexpected body: got %v want %v",
Expand Down
2 changes: 1 addition & 1 deletion authentication/repository/mock/user_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import (
)

var (
now = time.Now().Truncate(time.Millisecond)
now = user.NewTimestamp(time.Now())
u = user.User{
ID: "fake_user_id",
Username: "fake_username",
Expand Down
4 changes: 2 additions & 2 deletions authentication/repository/postgres/user_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import (
var (
// call `.Truncate` since the time is stored in db with a lower precision
// otherwise the assertion will result in a mismatch (in microseconds).
now = time.Now().Truncate(time.Millisecond)
now = user.NewTimestamp(time.Now())
u = user.User{
ID: "test_id",
Username: "test_username",
Expand Down Expand Up @@ -71,7 +71,7 @@ func TestUserRepository_Create(t *testing.T) {
ID: "fake_id_2",
Username: u.Username,
Password: "fake_password_2",
CreatedAt: time.Now(),
CreatedAt: user.NewTimestamp(time.Now()),
})
if err == nil {
t.Error("create user with duplicate username: expect error but got <nil>")
Expand Down
3 changes: 2 additions & 1 deletion authentication/user/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,11 +59,12 @@ func (svc *service) Signup(username, password string) (User, error) {
return User{}, fmt.Errorf("hash password: %v", err)
}

timestamp := NewTimestamp(time.Now())
u := User{
ID: ksuid.New().String(),
Username: username,
Password: p,
CreatedAt: time.Now().Truncate(time.Millisecond),
CreatedAt: timestamp,
}
err = svc.userRepo.Create(u)

Expand Down
36 changes: 36 additions & 0 deletions authentication/user/timestamp.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package user

import (
"fmt"
"time"
)

// we want to consistently format the time when serializing it
// e.g. when saving it to DB or sending it over HTTP

type timestamp time.Time

func NewTimestamp(t time.Time) timestamp {
return timestamp(t)
}

// Equal allows tools like `cmp` to compare two timestamps for equality
func (t timestamp) Equal(other timestamp) bool {
return t.String() == other.String()
}

// Scan implements Scanner interface supporting types: time.Time
func (t *timestamp) Scan(v interface{}) error {
if v == nil {
return nil
}
if v, ok := v.(time.Time); ok {
*t = NewTimestamp(v)
return nil
}
return fmt.Errorf("Can not scan %v to timestamp", v)
}

func (t timestamp) String() string {
return time.Time(t).Format(time.RFC3339)
}
40 changes: 40 additions & 0 deletions authentication/user/timestamp_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package user_test

import (
"testing"
"time"

"github.com/bhongy/kimidori/authentication/user"
)

const referenceTime = "2020-04-12T08:49:52Z"

func newReferenceTime(t *testing.T) time.Time {
tt, err := time.Parse(time.RFC3339, referenceTime)
if err != nil {
t.Fatalf("Cannot parse time: %q", tt)
}
return tt
}

// TODO: test Equal method

func Test_Timestamp_Scan(t *testing.T) {
ts := user.NewTimestamp(time.Now())
tt := newReferenceTime(t)
err := ts.Scan(tt)
if err != nil {
t.Error(err)
}
if got, want := ts.String(), referenceTime; got != want {
t.Errorf("Scanned value incorrect. want: %v, got: %v", got, want)
}
}

func Test_Timestamp_String(t *testing.T) {
tt := newReferenceTime(t)
ts := user.NewTimestamp(tt)
if formatted := ts.String(); formatted != referenceTime {
t.Error(formatted)
}
}
4 changes: 1 addition & 3 deletions authentication/user/user.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
package user

import "time"

// User models a user account in an authentication context.
type User struct {
ID string
Username string
Password password // a hashed password as stored in the database
CreatedAt time.Time
CreatedAt timestamp
}

0 comments on commit 9e3e4cb

Please sign in to comment.