idiomatic project structure
This commit is contained in:
108
cmd/main.go
Normal file
108
cmd/main.go
Normal file
@@ -0,0 +1,108 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/google/uuid"
|
||||
httpSwagger "github.com/swaggo/http-swagger"
|
||||
_ "go-nkode/docs"
|
||||
"go-nkode/internal/api"
|
||||
"go-nkode/internal/db"
|
||||
"go-nkode/internal/email"
|
||||
"go-nkode/internal/models"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
)
|
||||
|
||||
const (
|
||||
emailQueueBufferSize = 100
|
||||
maxEmailsPerSecond = 13 // SES allows 14, but I don't want to push it
|
||||
)
|
||||
|
||||
// @title NKode API
|
||||
// @version 1.0
|
||||
// @description This is the NKode API server.
|
||||
// @termsOfService http://nkode.example.com/terms/
|
||||
|
||||
// @contact.name API Support
|
||||
// @contact.url http://nkode.example.com/support
|
||||
// @contact.email support@nkode.example.com
|
||||
|
||||
// @license.name MIT
|
||||
// @license.url https://opensource.org/licenses/MIT
|
||||
|
||||
// @host localhost:8080
|
||||
// @BasePath /
|
||||
|
||||
// @securityDefinitions.apiKey ApiKeyAuth
|
||||
// @in header
|
||||
// @name Authorization
|
||||
|
||||
func main() {
|
||||
dbPath := os.Getenv("SQLITE_DB")
|
||||
if dbPath == "" {
|
||||
log.Fatalf("SQLITE_DB=/path/to/nkode.db not set")
|
||||
}
|
||||
db := db.NewSqliteDB(dbPath)
|
||||
defer db.CloseDb()
|
||||
|
||||
sesClient := email.NewSESClient()
|
||||
emailQueue := email.NewEmailQueue(emailQueueBufferSize, maxEmailsPerSecond, &sesClient)
|
||||
emailQueue.Start()
|
||||
defer emailQueue.Stop()
|
||||
|
||||
nkodeApi := api.NewNKodeAPI(db, emailQueue)
|
||||
AddDefaultCustomer(nkodeApi)
|
||||
handler := api.NKodeHandler{Api: nkodeApi}
|
||||
|
||||
mux := http.NewServeMux()
|
||||
mux.Handle(api.CreateNewCustomer, &handler)
|
||||
mux.Handle(api.GenerateSignupResetInterface, &handler)
|
||||
mux.Handle(api.SetNKode, &handler)
|
||||
mux.Handle(api.ConfirmNKode, &handler)
|
||||
mux.Handle(api.GetLoginInterface, &handler)
|
||||
mux.Handle(api.Login, &handler)
|
||||
mux.Handle(api.RenewAttributes, &handler)
|
||||
mux.Handle(api.RandomSvgInterface, &handler)
|
||||
mux.Handle(api.RefreshToken, &handler)
|
||||
mux.Handle(api.ResetNKode, &handler)
|
||||
|
||||
// Serve Swagger UI
|
||||
mux.Handle("/swagger/", httpSwagger.WrapHandler)
|
||||
|
||||
fmt.Println("Running on localhost:8080...")
|
||||
log.Fatal(http.ListenAndServe(":8080", corsMiddleware(mux)))
|
||||
}
|
||||
|
||||
func corsMiddleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// Set the CORS headers
|
||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS, PUT, DELETE")
|
||||
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
|
||||
|
||||
// Handle preflight requests
|
||||
if r.Method == http.MethodOptions {
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
return
|
||||
}
|
||||
|
||||
// Call the next handler
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
func AddDefaultCustomer(nkodeApi api.NKodeAPI) {
|
||||
newId, err := uuid.Parse("ed9ed6e0-082c-4b57-8d8c-f00ed6493457")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
customerId := models.CustomerId(newId)
|
||||
nkodePolicy := models.NewDefaultNKodePolicy()
|
||||
_, err = nkodeApi.CreateNewCustomer(nkodePolicy, &customerId)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
} else {
|
||||
log.Println("created new customer: ", newId)
|
||||
}
|
||||
}
|
||||
160
cmd/main_test.go
Normal file
160
cmd/main_test.go
Normal file
@@ -0,0 +1,160 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"go-nkode/internal/api"
|
||||
"go-nkode/internal/models"
|
||||
"go-nkode/internal/security"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestApi(t *testing.T) {
|
||||
base := "http://localhost:8080"
|
||||
newCustomerBody := models.NewCustomerPost{
|
||||
NKodePolicy: models.NewDefaultNKodePolicy(),
|
||||
}
|
||||
kp := models.KeypadDimension{
|
||||
AttrsPerKey: 14,
|
||||
NumbOfKeys: 10,
|
||||
}
|
||||
var customerResp models.CreateNewCustomerResp
|
||||
testApiPost(t, base+api.CreateNewCustomer, newCustomerBody, &customerResp)
|
||||
|
||||
userEmail := "test_username" + security.GenerateRandomString(12) + "@example.com"
|
||||
signupInterfaceBody := models.GenerateSignupRestInterfacePost{
|
||||
CustomerId: customerResp.CustomerId,
|
||||
AttrsPerKey: kp.AttrsPerKey,
|
||||
NumbOfKeys: kp.NumbOfKeys,
|
||||
UserEmail: strings.ToUpper(userEmail), // should be case-insensitive
|
||||
Reset: false,
|
||||
}
|
||||
var signupInterfaceResp models.GenerateSignupResetInterfaceResp
|
||||
testApiPost(t, base+api.GenerateSignupResetInterface, signupInterfaceBody, &signupInterfaceResp)
|
||||
assert.Len(t, signupInterfaceResp.SvgInterface, kp.TotalAttrs())
|
||||
passcodeLen := 4
|
||||
setInterface := signupInterfaceResp.UserIdxInterface
|
||||
userPasscode := setInterface[:passcodeLen]
|
||||
kpSet := models.KeypadDimension{NumbOfKeys: kp.NumbOfKeys, AttrsPerKey: kp.NumbOfKeys}
|
||||
setKeySelection, err := models.SelectKeyByAttrIdx(setInterface, userPasscode, kpSet)
|
||||
assert.NoError(t, err)
|
||||
setNKodeBody := models.SetNKodePost{
|
||||
CustomerId: customerResp.CustomerId,
|
||||
SessionId: signupInterfaceResp.SessionId,
|
||||
KeySelection: setKeySelection,
|
||||
}
|
||||
var setNKodeResp models.SetNKodeResp
|
||||
testApiPost(t, base+api.SetNKode, setNKodeBody, &setNKodeResp)
|
||||
confirmInterface := setNKodeResp.UserInterface
|
||||
confirmKeySelection, err := models.SelectKeyByAttrIdx(confirmInterface, userPasscode, kpSet)
|
||||
assert.NoError(t, err)
|
||||
confirmNKodeBody := models.ConfirmNKodePost{
|
||||
CustomerId: customerResp.CustomerId,
|
||||
KeySelection: confirmKeySelection,
|
||||
SessionId: signupInterfaceResp.SessionId,
|
||||
}
|
||||
testApiPost(t, base+api.ConfirmNKode, confirmNKodeBody, nil)
|
||||
|
||||
loginInterfaceBody := models.GetLoginInterfacePost{
|
||||
CustomerId: customerResp.CustomerId,
|
||||
UserEmail: userEmail,
|
||||
}
|
||||
|
||||
var loginInterfaceResp models.GetLoginInterfaceResp
|
||||
testApiPost(t, base+api.GetLoginInterface, loginInterfaceBody, &loginInterfaceResp)
|
||||
assert.Equal(t, loginInterfaceResp.AttrsPerKey, kp.AttrsPerKey)
|
||||
assert.Equal(t, loginInterfaceResp.NumbOfKeys, kp.NumbOfKeys)
|
||||
loginKeySelection, err := models.SelectKeyByAttrIdx(loginInterfaceResp.UserIdxInterface, userPasscode, kp)
|
||||
assert.NoError(t, err)
|
||||
loginBody := models.LoginPost{
|
||||
CustomerId: customerResp.CustomerId,
|
||||
UserEmail: userEmail,
|
||||
KeySelection: loginKeySelection,
|
||||
}
|
||||
|
||||
var jwtTokens security.AuthenticationTokens
|
||||
testApiPost(t, base+api.Login, loginBody, &jwtTokens)
|
||||
refreshClaims, err := security.ParseRegisteredClaimToken(jwtTokens.RefreshToken)
|
||||
assert.Equal(t, refreshClaims.Subject, userEmail)
|
||||
accessClaims, err := security.ParseRegisteredClaimToken(jwtTokens.AccessToken)
|
||||
assert.Equal(t, accessClaims.Subject, userEmail)
|
||||
renewBody := models.RenewAttributesPost{CustomerId: customerResp.CustomerId}
|
||||
testApiPost(t, base+api.RenewAttributes, renewBody, nil)
|
||||
|
||||
loginKeySelection, err = models.SelectKeyByAttrIdx(loginInterfaceResp.UserIdxInterface, userPasscode, kp)
|
||||
assert.NoError(t, err)
|
||||
loginBody = models.LoginPost{
|
||||
CustomerId: customerResp.CustomerId,
|
||||
UserEmail: userEmail,
|
||||
KeySelection: loginKeySelection,
|
||||
}
|
||||
|
||||
testApiPost(t, base+api.Login, loginBody, &jwtTokens)
|
||||
|
||||
var randomSvgInterfaceResp models.RandomSvgInterfaceResp
|
||||
testApiGet(t, base+api.RandomSvgInterface, &randomSvgInterfaceResp, "")
|
||||
assert.Equal(t, models.KeypadMax.TotalAttrs(), len(randomSvgInterfaceResp.Svgs))
|
||||
|
||||
var refreshTokenResp models.RefreshTokenResp
|
||||
|
||||
testApiGet(t, base+api.RefreshToken, &refreshTokenResp, jwtTokens.RefreshToken)
|
||||
accessClaims, err = security.ParseRegisteredClaimToken(refreshTokenResp.AccessToken)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, accessClaims.Subject, userEmail)
|
||||
}
|
||||
|
||||
func Unmarshal(t *testing.T, resp *http.Response, data any) {
|
||||
responseBody, err := io.ReadAll(resp.Body)
|
||||
assert.NoError(t, err)
|
||||
err = json.Unmarshal(responseBody, data)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func Marshal(t *testing.T, data any) *bytes.Reader {
|
||||
jsonBytes, err := json.Marshal(data)
|
||||
assert.NoError(t, err)
|
||||
reader := bytes.NewReader(jsonBytes)
|
||||
return reader
|
||||
}
|
||||
|
||||
func testApiPost(t *testing.T, endpointStr string, postBody any, respBody any) {
|
||||
reader := Marshal(t, postBody)
|
||||
resp, err := http.Post(endpointStr, "application/json", reader)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
if respBody != nil {
|
||||
Unmarshal(t, resp, respBody)
|
||||
}
|
||||
}
|
||||
|
||||
func testApiGet(t *testing.T, endpointStr string, respBody any, bearerToken string) {
|
||||
req, err := http.NewRequest("GET", endpointStr, nil)
|
||||
if err != nil {
|
||||
fmt.Println("Error creating request:", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Add the Bearer token to the Authorization header
|
||||
if bearerToken != "" {
|
||||
req.Header.Set("Authorization", "Bearer "+bearerToken)
|
||||
}
|
||||
// Make the HTTP request
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
fmt.Println("Error making request:", err)
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, resp.StatusCode, http.StatusOK)
|
||||
if respBody != nil {
|
||||
Unmarshal(t, resp, respBody)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user