|
| 1 | +package example_test |
| 2 | + |
| 3 | +import ( |
| 4 | + "bytes" |
| 5 | + "encoding/json" |
| 6 | + "net/http" |
| 7 | + "net/http/httptest" |
| 8 | + "testing" |
| 9 | + |
| 10 | + "github.com/gin-gonic/gin" |
| 11 | + "github.com/philiphil/restman/configuration" |
| 12 | + "github.com/philiphil/restman/orm" |
| 13 | + "github.com/philiphil/restman/orm/entity" |
| 14 | + "github.com/philiphil/restman/orm/gormrepository" |
| 15 | + "github.com/philiphil/restman/route" |
| 16 | + "github.com/philiphil/restman/router" |
| 17 | + "gorm.io/driver/sqlite" |
| 18 | + "gorm.io/gorm" |
| 19 | + "gorm.io/gorm/logger" |
| 20 | +) |
| 21 | + |
| 22 | +//Serialization groups are an easy way to control which fields are included in API responses and requests based on context. |
| 23 | +// In this example, we define "read" and "write" groups to manage field visibility during GET and POST/PUT operations respectively. |
| 24 | + |
| 25 | +// With SerializationGroups configuration , as with any configuration we can set router-wide defaults that can be overridden on a operation level |
| 26 | +// we also provide client control over group overwriting via query parameters. |
| 27 | + |
| 28 | +type SerialProduct struct { |
| 29 | + entity.BaseEntity |
| 30 | + Name string `json:"name" groups:"read,write"` |
| 31 | + Price float64 `json:"price" groups:"write"` |
| 32 | + InternalSKU string `json:"internal_sku" groups:"read"` //should be never editable |
| 33 | + CostPrice float64 `json:"cost_price" groups:"private_stuff"` //should be never visible via api |
| 34 | +} |
| 35 | + |
| 36 | +func (p SerialProduct) GetId() entity.ID { return p.Id } |
| 37 | +func (p SerialProduct) SetId(id any) entity.Entity { |
| 38 | + p.Id = entity.CastId(id) |
| 39 | + return p |
| 40 | +} |
| 41 | +func (p SerialProduct) ToEntity() SerialProduct { return p } |
| 42 | +func (p SerialProduct) FromEntity(e SerialProduct) any { return e } |
| 43 | + |
| 44 | +func getSerialProductDB() *gorm.DB { |
| 45 | + db, err := gorm.Open(sqlite.Open("file:serial_product_test?mode=memory&cache=shared&_fk=1"), &gorm.Config{ |
| 46 | + Logger: logger.Default.LogMode(logger.Silent), |
| 47 | + CreateBatchSize: 1000, |
| 48 | + }) |
| 49 | + if err != nil { |
| 50 | + panic(err) |
| 51 | + } |
| 52 | + return db |
| 53 | +} |
| 54 | + |
| 55 | +func TestSerializationGroupsReadWrite(t *testing.T) { |
| 56 | + db := getSerialProductDB() |
| 57 | + db.AutoMigrate(&SerialProduct{}) |
| 58 | + |
| 59 | + r := gin.New() |
| 60 | + r.Use(gin.Recovery()) |
| 61 | + |
| 62 | + routes := route.DefaultApiRoutes() |
| 63 | + routes[route.Post].Configuration[configuration.InputSerializationGroupsType] = configuration.InputSerializationGroups("write") |
| 64 | + |
| 65 | + productRouter := router.NewApiRouter( |
| 66 | + *orm.NewORM(gormrepository.NewRepository[SerialProduct](db)), |
| 67 | + routes, |
| 68 | + configuration.OutputSerializationGroups("read"), //default router-wide serialization group should be "read" |
| 69 | + ) |
| 70 | + |
| 71 | + productRouter.AllowRoutes(r) |
| 72 | + |
| 73 | + postData := SerialProduct{ |
| 74 | + Name: "Laptop", |
| 75 | + Price: 999.99, |
| 76 | + CostPrice: 700.00, |
| 77 | + InternalSKU: "SKU-IGNORED", |
| 78 | + } |
| 79 | + jsonData, _ := json.Marshal(postData) |
| 80 | + |
| 81 | + w := httptest.NewRecorder() |
| 82 | + req, _ := http.NewRequest("POST", "/api/serial_product", bytes.NewBuffer(jsonData)) |
| 83 | + req.Header.Set("Content-Type", "application/json") |
| 84 | + r.ServeHTTP(w, req) |
| 85 | + |
| 86 | + if w.Code != http.StatusCreated { |
| 87 | + t.Errorf("Expected status 201, got %d. Body: %s", w.Code, w.Body.String()) |
| 88 | + } |
| 89 | + |
| 90 | + var createdProduct SerialProduct |
| 91 | + db.First(&createdProduct, 1) |
| 92 | + |
| 93 | + if createdProduct.Name != "Laptop" { |
| 94 | + t.Errorf("Name should have been written, got: %s", createdProduct.Name) |
| 95 | + } |
| 96 | + |
| 97 | + if createdProduct.Price != 999.99 { |
| 98 | + t.Errorf("Price should have been written to DB, got: %f", createdProduct.Price) |
| 99 | + } |
| 100 | + |
| 101 | + if createdProduct.InternalSKU == "SKU-IGNORED" { |
| 102 | + t.Error("InternalSKU should not have been written (not in write group)") |
| 103 | + } |
| 104 | + |
| 105 | + if createdProduct.CostPrice == 700.00 { |
| 106 | + t.Error("CostPrice should not have been written (not in write group)") |
| 107 | + } |
| 108 | + |
| 109 | + w = httptest.NewRecorder() |
| 110 | + req, _ = http.NewRequest("GET", "/api/serial_product/1", nil) |
| 111 | + r.ServeHTTP(w, req) |
| 112 | + |
| 113 | + if w.Code != http.StatusOK { |
| 114 | + t.Errorf("Expected status 200, got %d", w.Code) |
| 115 | + } |
| 116 | + |
| 117 | + var response SerialProduct |
| 118 | + json.Unmarshal(w.Body.Bytes(), &response) |
| 119 | + |
| 120 | + if response.Name != "Laptop" { |
| 121 | + t.Error("Name should be visible in GET response (read group)") |
| 122 | + } |
| 123 | + |
| 124 | + if response.Price != 0 { |
| 125 | + t.Errorf("Price should NOT be visible in GET response (write group only), got: %f", response.Price) |
| 126 | + } |
| 127 | + |
| 128 | + if response.InternalSKU != "" { |
| 129 | + t.Errorf("InternalSKU should be empty in GET (read group but never written), got: %s", response.InternalSKU) |
| 130 | + } |
| 131 | + |
| 132 | + if response.CostPrice != 0 { |
| 133 | + t.Errorf("CostPrice should be zero in GET (private_stuff group not included), got: %f", response.CostPrice) |
| 134 | + } |
| 135 | +} |
0 commit comments