476 lines
15 KiB
Go
476 lines
15 KiB
Go
package service
|
|
|
|
import (
|
|
"archive/zip"
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestAdminAppIDUpdateClearsCachedToken(t *testing.T) {
|
|
service, err := NewService(t.TempDir() + "/onelink_mock.db")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
appid := service.store.Snapshot().AppIDs[0].AppID
|
|
if err := service.store.SaveMutation(func(st *Store) error {
|
|
st.Tokens[appid] = Token{AppID: appid, Value: "old-token", ExpiresAt: time.Now().Add(time.Hour)}
|
|
st.AppIDs[0].LastToken = "old-token"
|
|
st.AppIDs[0].TokenExpires = time.Now().Add(time.Hour).Format("2006-01-02 15:04:05")
|
|
return nil
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
body := strings.NewReader(`{"appid":"` + appid + `","password":"demo_password","name":"default"}`)
|
|
req := httptest.NewRequest(http.MethodPost, "/api/appids", body)
|
|
req.Header.Set("Content-Type", "application/json")
|
|
res := httptest.NewRecorder()
|
|
|
|
service.Router().ServeHTTP(res, req)
|
|
|
|
if res.Code != http.StatusOK {
|
|
t.Fatalf("status = %d body=%s", res.Code, res.Body.String())
|
|
}
|
|
st := service.store.Snapshot()
|
|
if _, ok := st.Tokens[appid]; ok {
|
|
t.Fatalf("cached token was not cleared: %#v", st.Tokens[appid])
|
|
}
|
|
if st.AppIDs[0].LastToken != "" || st.AppIDs[0].TokenExpires != "" {
|
|
t.Fatalf("appid token display was not cleared: %#v", st.AppIDs[0])
|
|
}
|
|
}
|
|
|
|
func TestAdminCardsExportMarksExportedCardsAndPersists(t *testing.T) {
|
|
dbPath := t.TempDir() + "/onelink_mock.db"
|
|
service, err := NewService(dbPath)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := service.store.GenerateCards(2); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
cards := service.store.Snapshot().Cards
|
|
req := httptest.NewRequest(http.MethodGet, "/api/cards/export?msisdns="+cards[0].MSISDN, nil)
|
|
res := httptest.NewRecorder()
|
|
service.Router().ServeHTTP(res, req)
|
|
if res.Code != http.StatusOK {
|
|
t.Fatalf("status = %d body=%s", res.Code, res.Body.String())
|
|
}
|
|
|
|
exportedPage := service.store.QueryCards("", "exported", 1, 20)
|
|
if exportedPage.Total != 1 || exportedPage.Items[0].MSISDN != cards[0].MSISDN {
|
|
t.Fatalf("exported cards = %#v", exportedPage)
|
|
}
|
|
if all := service.store.QueryCards("", "", 1, 20); len(all.Items) != 2 || all.Items[0].MSISDN != cards[1].MSISDN {
|
|
t.Fatalf("cards are not newest-first: %#v", all.Items)
|
|
}
|
|
|
|
reloaded, err := NewService(dbPath)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
persisted := reloaded.store.QueryCards("", "exported", 1, 20)
|
|
if persisted.Total != 1 || persisted.Items[0].MSISDN != cards[0].MSISDN || persisted.Items[0].ExportCheckedAt == "" {
|
|
t.Fatalf("persisted exported cards = %#v", persisted)
|
|
}
|
|
}
|
|
|
|
func TestAdminCardRegionLimitStatusPersistsAndQueries(t *testing.T) {
|
|
dbPath := t.TempDir() + "/onelink_mock.db"
|
|
service, err := NewService(dbPath)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := service.store.GenerateCards(1); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
card := service.store.Snapshot().Cards[0]
|
|
card.RegionLimitStatus = "1"
|
|
body, err := json.Marshal(card)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
res := httptest.NewRecorder()
|
|
req := httptest.NewRequest(http.MethodPost, "/api/cards", bytes.NewReader(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
service.Router().ServeHTTP(res, req)
|
|
if res.Code != http.StatusOK {
|
|
t.Fatalf("save status = %d body=%s", res.Code, res.Body.String())
|
|
}
|
|
|
|
reloaded, err := NewService(dbPath)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got := reloaded.store.Snapshot().Cards[0].RegionLimitStatus; got != "1" {
|
|
t.Fatalf("persisted region limit status = %q, want 1", got)
|
|
}
|
|
token := "region-limit-test-token"
|
|
if err := reloaded.store.SaveMutation(func(st *Store) error {
|
|
st.Tokens[st.AppIDs[0].AppID] = Token{Value: token, ExpiresAt: time.Now().Add(time.Hour)}
|
|
return nil
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
query := func(path string, target any) {
|
|
method := http.MethodGet
|
|
var requestBody *bytes.Reader
|
|
if strings.HasPrefix(path, "/sim/") {
|
|
method = http.MethodPost
|
|
requestBody = bytes.NewReader([]byte(`{"simKeys":[{"msisdn":"` + card.MSISDN + `"}],"reqData":{}}`))
|
|
} else {
|
|
requestBody = bytes.NewReader(nil)
|
|
}
|
|
queryString := ""
|
|
if method == http.MethodGet {
|
|
queryString = "?token=" + token + "&msisdn=" + card.MSISDN
|
|
}
|
|
req := httptest.NewRequest(method, path+queryString, requestBody)
|
|
req.Header.Set("Content-Type", "application/json")
|
|
res := httptest.NewRecorder()
|
|
reloaded.Router().ServeHTTP(res, req)
|
|
if res.Code != http.StatusOK {
|
|
t.Fatalf("%s status = %d body=%s", path, res.Code, res.Body.String())
|
|
}
|
|
if err := json.Unmarshal(res.Body.Bytes(), target); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
var oneLink Response
|
|
query("/v5/ec/query/region-limit-status", &oneLink)
|
|
result, ok := oneLink.Result[0].(map[string]any)
|
|
if !ok || len(oneLink.Result) != 1 || result["regionLimitStatus"] != "1" {
|
|
t.Fatalf("one link region response = %#v", oneLink)
|
|
}
|
|
var chanResponses []chanResponse[chanRegionLimitStatus]
|
|
query("/sim/query/sim-region-limit-status", &chanResponses)
|
|
if len(chanResponses) != 1 || chanResponses[0].ResData.RegionLimitStatus != "1" {
|
|
t.Fatalf("chan region response = %#v", chanResponses)
|
|
}
|
|
}
|
|
|
|
func TestAdminGenerateAppendsRequestedCount(t *testing.T) {
|
|
service, err := NewService(t.TempDir() + "/onelink_mock.db")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
body := strings.NewReader(`{"count":2}`)
|
|
req := httptest.NewRequest(http.MethodPost, "/api/generate", body)
|
|
req.Header.Set("Content-Type", "application/json")
|
|
res := httptest.NewRecorder()
|
|
|
|
service.Router().ServeHTTP(res, req)
|
|
|
|
if res.Code != http.StatusOK {
|
|
t.Fatalf("status = %d body=%s", res.Code, res.Body.String())
|
|
}
|
|
var result GenerateResult
|
|
if err := json.Unmarshal(res.Body.Bytes(), &result); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if result.Generated != 2 || result.Total != 2 {
|
|
t.Fatalf("result = %#v, want generated 2 total 2", result)
|
|
}
|
|
if got := service.store.Snapshot(); len(got.Cards) != 2 {
|
|
t.Fatalf("store cards = %d, want 2", len(got.Cards))
|
|
}
|
|
}
|
|
|
|
func TestAdminGenerateAssignsSelectedGroup(t *testing.T) {
|
|
service, err := NewService(t.TempDir() + "/onelink_mock.db")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
create := httptest.NewRequest(http.MethodPost, "/api/card-groups", strings.NewReader(`{"name":"批次一"}`))
|
|
create.Header.Set("Content-Type", "application/json")
|
|
createdRes := httptest.NewRecorder()
|
|
service.Router().ServeHTTP(createdRes, create)
|
|
if createdRes.Code != http.StatusOK {
|
|
t.Fatalf("create group status = %d body=%s", createdRes.Code, createdRes.Body.String())
|
|
}
|
|
var group CardGroup
|
|
if err := json.Unmarshal(createdRes.Body.Bytes(), &group); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
generate := httptest.NewRequest(http.MethodPost, "/api/generate", strings.NewReader(fmt.Sprintf(`{"count":2,"groupId":%d}`, group.ID)))
|
|
generate.Header.Set("Content-Type", "application/json")
|
|
res := httptest.NewRecorder()
|
|
service.Router().ServeHTTP(res, generate)
|
|
if res.Code != http.StatusOK {
|
|
t.Fatalf("generate status = %d body=%s", res.Code, res.Body.String())
|
|
}
|
|
for _, card := range service.store.Snapshot().Cards {
|
|
if card.GroupID != group.ID || card.GroupName != group.Name {
|
|
t.Fatalf("card group = %#v, want %d/%s", card, group.ID, group.Name)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestAdminCardsQueriesByAnyExactIdentifier(t *testing.T) {
|
|
service, err := NewService(t.TempDir() + "/onelink_mock.db")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := service.store.GenerateCards(2); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
cards := service.store.Snapshot().Cards
|
|
|
|
for _, keyword := range []string{cards[0].MSISDN, cards[0].ICCID, cards[0].IMSI} {
|
|
t.Run(keyword, func(t *testing.T) {
|
|
req := httptest.NewRequest(http.MethodGet, "/api/cards?keyword="+keyword, nil)
|
|
res := httptest.NewRecorder()
|
|
service.Router().ServeHTTP(res, req)
|
|
if res.Code != http.StatusOK {
|
|
t.Fatalf("status = %d body=%s", res.Code, res.Body.String())
|
|
}
|
|
var result struct {
|
|
Items []SIMCard `json:"items"`
|
|
Total int `json:"total"`
|
|
}
|
|
if err := json.Unmarshal(res.Body.Bytes(), &result); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if result.Total != 1 || len(result.Items) != 1 || result.Items[0].MSISDN != cards[0].MSISDN {
|
|
t.Fatalf("result = %#v, want the exact matching card", result)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestAdminCardsQueryReturnsEmptyPageWhenNotFound(t *testing.T) {
|
|
service, err := NewService(t.TempDir() + "/onelink_mock.db")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
req := httptest.NewRequest(http.MethodGet, "/api/cards?keyword=not-found", nil)
|
|
res := httptest.NewRecorder()
|
|
service.Router().ServeHTTP(res, req)
|
|
if res.Code != http.StatusOK {
|
|
t.Fatalf("status = %d body=%s", res.Code, res.Body.String())
|
|
}
|
|
var result struct {
|
|
Items []SIMCard `json:"items"`
|
|
Total int `json:"total"`
|
|
}
|
|
if err := json.Unmarshal(res.Body.Bytes(), &result); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if result.Items == nil || result.Total != 0 || len(result.Items) != 0 {
|
|
t.Fatalf("result = %#v, want an empty page", result)
|
|
}
|
|
}
|
|
|
|
func TestAdminCardsExportUsesInboundExcelHeaders(t *testing.T) {
|
|
service, err := NewService(t.TempDir() + "/onelink_mock.db")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := service.store.GenerateCards(1); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
req := httptest.NewRequest(http.MethodGet, "/api/cards/export", nil)
|
|
res := httptest.NewRecorder()
|
|
|
|
service.Router().ServeHTTP(res, req)
|
|
|
|
if res.Code != http.StatusOK {
|
|
t.Fatalf("status = %d body=%s", res.Code, res.Body.String())
|
|
}
|
|
if contentType := res.Header().Get("Content-Type"); contentType != "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" {
|
|
t.Fatalf("content type = %q", contentType)
|
|
}
|
|
workbookText := unzipWorkbookText(t, res.Body.Bytes())
|
|
for _, header := range inboundExportHeaders {
|
|
if !strings.Contains(workbookText, header) {
|
|
t.Fatalf("export workbook missing header %q in %s", header, workbookText)
|
|
}
|
|
}
|
|
if !strings.Contains(workbookText, "channel_yd_mock") || !strings.Contains(workbookText, "CMIOT-SD-500M-24M-D01") {
|
|
t.Fatalf("export workbook missing expected generated values: %s", workbookText)
|
|
}
|
|
for _, marker := range []string{"sharedStrings.xml", "styles.xml"} {
|
|
if !zipContainsFile(t, res.Body.Bytes(), "xl/"+marker) {
|
|
t.Fatalf("export workbook missing xl/%s", marker)
|
|
}
|
|
}
|
|
}
|
|
|
|
func zipContainsFile(t *testing.T, raw []byte, name string) bool {
|
|
t.Helper()
|
|
reader, err := zip.NewReader(bytes.NewReader(raw), int64(len(raw)))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, file := range reader.File {
|
|
if file.Name == name {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func TestAdminCardsExportFiltersSelectedMSISDNs(t *testing.T) {
|
|
service, err := NewService(t.TempDir() + "/onelink_mock.db")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := service.store.GenerateCards(3); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
cards := service.store.Snapshot().Cards
|
|
selected := cards[0].MSISDN + "," + cards[2].MSISDN
|
|
req := httptest.NewRequest(http.MethodGet, "/api/cards/export?msisdns="+selected, nil)
|
|
res := httptest.NewRecorder()
|
|
|
|
service.Router().ServeHTTP(res, req)
|
|
|
|
if res.Code != http.StatusOK {
|
|
t.Fatalf("status = %d body=%s", res.Code, res.Body.String())
|
|
}
|
|
workbookText := unzipWorkbookText(t, res.Body.Bytes())
|
|
if !strings.Contains(workbookText, cards[0].MSISDN) || !strings.Contains(workbookText, cards[2].MSISDN) {
|
|
t.Fatalf("export workbook missing selected cards: %s", workbookText)
|
|
}
|
|
if strings.Contains(workbookText, cards[1].MSISDN) {
|
|
t.Fatalf("export workbook included unselected card %s: %s", cards[1].MSISDN, workbookText)
|
|
}
|
|
}
|
|
|
|
func TestAdminCardsExportFilteredResultsAcrossPages(t *testing.T) {
|
|
service, err := NewService(t.TempDir() + "/onelink_mock.db")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
createdAt := time.Now().Format("2006-01-02 15:04:05")
|
|
if err = service.store.SaveMutation(func(st *Store) error {
|
|
st.Groups = append(st.Groups,
|
|
CardGroup{ID: 7, Name: "group-seven", CreatedAt: createdAt, UpdatedAt: createdAt},
|
|
CardGroup{ID: 8, Name: "group-eight", CreatedAt: createdAt, UpdatedAt: createdAt},
|
|
)
|
|
return nil
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err = service.store.GenerateCardsInGroup(25, 7); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err = service.store.GenerateCardsInGroup(5, 8); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
groupSevenCards := service.store.QueryAllCardsByGroup("", "", 7, true)
|
|
if len(groupSevenCards) != 25 {
|
|
t.Fatalf("group 7 cards = %d, want 25", len(groupSevenCards))
|
|
}
|
|
alreadyExported := groupSevenCards[0].MSISDN
|
|
if err = service.store.SaveMutation(func(st *Store) error {
|
|
for i := range st.Cards {
|
|
if st.Cards[i].MSISDN == alreadyExported {
|
|
st.Cards[i].Exported = true
|
|
st.Cards[i].ExportCheckedAt = "2026-08-27 00:00:00"
|
|
break
|
|
}
|
|
}
|
|
return nil
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/api/cards/export?selection=filtered&groupId=7&exportStatus=not_exported", nil)
|
|
res := httptest.NewRecorder()
|
|
service.Router().ServeHTTP(res, req)
|
|
|
|
if res.Code != http.StatusOK {
|
|
t.Fatalf("status = %d body=%s", res.Code, res.Body.String())
|
|
}
|
|
workbookText := unzipWorkbookText(t, res.Body.Bytes())
|
|
for _, card := range groupSevenCards[1:] {
|
|
if !strings.Contains(workbookText, card.MSISDN) {
|
|
t.Fatalf("export workbook missing filtered card %s", card.MSISDN)
|
|
}
|
|
}
|
|
if strings.Contains(workbookText, alreadyExported) {
|
|
t.Fatalf("export workbook included already exported card %s", alreadyExported)
|
|
}
|
|
for _, card := range service.store.QueryAllCardsByGroup("", "", 8, true) {
|
|
if strings.Contains(workbookText, card.MSISDN) {
|
|
t.Fatalf("export workbook included card from another group %s", card.MSISDN)
|
|
}
|
|
}
|
|
if exported := service.store.QueryAllCardsByGroup("", "exported", 7, true); len(exported) != 25 {
|
|
t.Fatalf("exported group 7 cards = %d, want 25", len(exported))
|
|
}
|
|
if exported := service.store.QueryAllCardsByGroup("", "exported", 8, true); len(exported) != 0 {
|
|
t.Fatalf("exported group 8 cards = %d, want 0", len(exported))
|
|
}
|
|
}
|
|
|
|
func TestAdminCardsExportFilteredResultsByKeyword(t *testing.T) {
|
|
service, err := NewService(t.TempDir() + "/onelink_mock.db")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err = service.store.GenerateCards(3); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
cards := service.store.Snapshot().Cards
|
|
req := httptest.NewRequest(http.MethodGet, "/api/cards/export?selection=filtered&keyword="+cards[1].ICCID, nil)
|
|
res := httptest.NewRecorder()
|
|
service.Router().ServeHTTP(res, req)
|
|
|
|
if res.Code != http.StatusOK {
|
|
t.Fatalf("status = %d body=%s", res.Code, res.Body.String())
|
|
}
|
|
workbookText := unzipWorkbookText(t, res.Body.Bytes())
|
|
if !strings.Contains(workbookText, cards[1].MSISDN) {
|
|
t.Fatalf("export workbook missing keyword match %s", cards[1].MSISDN)
|
|
}
|
|
for _, card := range []SIMCard{cards[0], cards[2]} {
|
|
if strings.Contains(workbookText, card.MSISDN) {
|
|
t.Fatalf("export workbook included non-matching card %s", card.MSISDN)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestAdminCardsExportFilteredResultsRejectsInvalidFilter(t *testing.T) {
|
|
service, err := NewService(t.TempDir() + "/onelink_mock.db")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
req := httptest.NewRequest(http.MethodGet, "/api/cards/export?selection=filtered&exportStatus=invalid", nil)
|
|
res := httptest.NewRecorder()
|
|
service.Router().ServeHTTP(res, req)
|
|
if res.Code != http.StatusBadRequest {
|
|
t.Fatalf("status = %d body=%s", res.Code, res.Body.String())
|
|
}
|
|
}
|
|
|
|
func unzipWorkbookText(t *testing.T, raw []byte) string {
|
|
t.Helper()
|
|
reader, err := zip.NewReader(bytes.NewReader(raw), int64(len(raw)))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var out strings.Builder
|
|
for _, file := range reader.File {
|
|
if !strings.HasSuffix(file.Name, ".xml") {
|
|
continue
|
|
}
|
|
rc, err := file.Open()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
_, _ = io.Copy(&out, rc)
|
|
_ = rc.Close()
|
|
}
|
|
return out.String()
|
|
}
|