mirror of
https://github.com/opencloud-eu/opencloud.git
synced 2026-05-01 20:53:05 -04:00
* move ContactCard from jscontact to jmap, as it is actually a JMAP specification item, but also was causing too many issues with circular references from jscontact -> jmap * introduce Foo, Idable, GetRequest, GetResponse, etc... types and generics * first attempt at a Foo factory type for Mailboxes, needs to be expanded to further minimize repetition * add more specialized template functions to avoid repetition * introduce ChangesTemplate[T] for *Changes structs
661 lines
14 KiB
Go
661 lines
14 KiB
Go
package jscontact
|
||
|
||
import (
|
||
"encoding/json"
|
||
"testing"
|
||
"time"
|
||
|
||
"github.com/stretchr/testify/require"
|
||
)
|
||
|
||
func jsoneq[X any](t *testing.T, expected string, object X) {
|
||
data, err := json.MarshalIndent(object, "", "")
|
||
require.NoError(t, err)
|
||
require.JSONEq(t, expected, string(data))
|
||
|
||
var rec X
|
||
err = json.Unmarshal(data, &rec)
|
||
require.NoError(t, err)
|
||
require.Equal(t, object, rec)
|
||
}
|
||
|
||
func TestCalendar(t *testing.T) {
|
||
jsoneq(t, `{
|
||
"@type": "Calendar",
|
||
"kind": "calendar",
|
||
"uri": "https://opencloud.eu/calendar/d05779b6-9638-4694-9869-008a61df6025",
|
||
"mediaType": "application/jscontact+json",
|
||
"contexts": {
|
||
"work": true
|
||
},
|
||
"label": "test"
|
||
}`, Calendar{
|
||
Type: CalendarType,
|
||
Kind: CalendarKindCalendar,
|
||
Uri: "https://opencloud.eu/calendar/d05779b6-9638-4694-9869-008a61df6025", //NOSONAR
|
||
MediaType: "application/jscontact+json", //NOSONAR
|
||
Contexts: map[CalendarContext]bool{
|
||
CalendarContextWork: true,
|
||
},
|
||
Pref: 0,
|
||
Label: "test",
|
||
})
|
||
}
|
||
|
||
func TestLink(t *testing.T) {
|
||
jsoneq(t, `{
|
||
"@type": "Link",
|
||
"kind": "contact",
|
||
"uri": "https://opencloud.eu/calendar/d05779b6-9638-4694-9869-008a61df6025",
|
||
"mediaType": "application/jscontact+json",
|
||
"contexts": {
|
||
"work": true
|
||
},
|
||
"label": "test"
|
||
}`, Link{
|
||
Type: LinkType,
|
||
Kind: LinkKindContact,
|
||
Uri: "https://opencloud.eu/calendar/d05779b6-9638-4694-9869-008a61df6025",
|
||
MediaType: "application/jscontact+json",
|
||
Contexts: map[LinkContext]bool{
|
||
LinkContextWork: true,
|
||
},
|
||
Pref: 0,
|
||
Label: "test",
|
||
})
|
||
}
|
||
|
||
func TestCryptoKey(t *testing.T) {
|
||
jsoneq(t, `{
|
||
"@type": "CryptoKey",
|
||
"uri": "https://opencloud.eu/calendar/d05779b6-9638-4694-9869-008a61df6025.pgp",
|
||
"mediaType": "application/pgp-keys",
|
||
"contexts": {
|
||
"work": true
|
||
},
|
||
"label": "test"
|
||
}`, CryptoKey{
|
||
Type: CryptoKeyType,
|
||
Uri: "https://opencloud.eu/calendar/d05779b6-9638-4694-9869-008a61df6025.pgp",
|
||
MediaType: "application/pgp-keys",
|
||
Contexts: map[CryptoKeyContext]bool{
|
||
CryptoKeyContextWork: true,
|
||
},
|
||
Pref: 0,
|
||
Label: "test",
|
||
})
|
||
}
|
||
|
||
func TestDirectory(t *testing.T) {
|
||
jsoneq(t, `{
|
||
"@type": "Directory",
|
||
"kind": "entry",
|
||
"uri": "https://opencloud.eu/calendar/d05779b6-9638-4694-9869-008a61df6025",
|
||
"mediaType": "application/jscontact+json",
|
||
"contexts": {
|
||
"work": true
|
||
},
|
||
"label": "test",
|
||
"listAs": 3
|
||
}`, Directory{
|
||
Type: DirectoryType,
|
||
Kind: DirectoryKindEntry,
|
||
Uri: "https://opencloud.eu/calendar/d05779b6-9638-4694-9869-008a61df6025",
|
||
MediaType: "application/jscontact+json",
|
||
Contexts: map[DirectoryContext]bool{
|
||
DirectoryContextWork: true,
|
||
},
|
||
Pref: 0,
|
||
Label: "test",
|
||
ListAs: 3,
|
||
})
|
||
}
|
||
|
||
func TestMedia(t *testing.T) {
|
||
jsoneq(t, `{
|
||
"@type": "Media",
|
||
"kind": "logo",
|
||
"uri": "https://opencloud.eu/opencloud.svg",
|
||
"mediaType": "image/svg+xml",
|
||
"contexts": {
|
||
"work": true
|
||
},
|
||
"label": "test",
|
||
"blobId": "1d92cf97e32b42ceb5538f0804a41891"
|
||
}`, Media{
|
||
Type: MediaType,
|
||
Kind: MediaKindLogo,
|
||
Uri: "https://opencloud.eu/opencloud.svg",
|
||
MediaType: "image/svg+xml",
|
||
Contexts: map[MediaContext]bool{
|
||
MediaContextWork: true,
|
||
},
|
||
Pref: 0,
|
||
Label: "test",
|
||
BlobId: "1d92cf97e32b42ceb5538f0804a41891",
|
||
})
|
||
}
|
||
|
||
func TestRelation(t *testing.T) {
|
||
jsoneq(t, `{
|
||
"@type": "Relation",
|
||
"relation": {
|
||
"co-worker": true,
|
||
"friend": true
|
||
}
|
||
}`, Relation{
|
||
Type: RelationType,
|
||
Relation: map[Relationship]bool{
|
||
RelationCoWorker: true,
|
||
RelationFriend: true,
|
||
},
|
||
})
|
||
}
|
||
|
||
func TestNameComponent(t *testing.T) {
|
||
jsoneq(t, `{
|
||
"@type": "NameComponent",
|
||
"value": "Robert",
|
||
"kind": "given",
|
||
"phonetic": "Bob"
|
||
}`, NameComponent{
|
||
Type: NameComponentType,
|
||
Value: "Robert",
|
||
Kind: NameComponentKindGiven,
|
||
Phonetic: "Bob",
|
||
})
|
||
}
|
||
|
||
func TestNickname(t *testing.T) {
|
||
jsoneq(t, `{
|
||
"@type": "Nickname",
|
||
"name": "Bob",
|
||
"contexts": {
|
||
"private": true
|
||
},
|
||
"pref": 3
|
||
}`, Nickname{
|
||
Type: NicknameType,
|
||
Name: "Bob",
|
||
Contexts: map[NicknameContext]bool{
|
||
NicknameContextPrivate: true,
|
||
},
|
||
Pref: 3,
|
||
})
|
||
}
|
||
|
||
func TestOrgUnit(t *testing.T) {
|
||
jsoneq(t, `{
|
||
"@type": "OrgUnit",
|
||
"name": "Skynet",
|
||
"sortAs": "SKY"
|
||
}`, OrgUnit{
|
||
Type: OrgUnitType,
|
||
Name: "Skynet",
|
||
SortAs: "SKY",
|
||
})
|
||
}
|
||
|
||
func TestOrganization(t *testing.T) {
|
||
jsoneq(t, `{
|
||
"@type": "Organization",
|
||
"name": "Cyberdyne",
|
||
"sortAs": "CYBER",
|
||
"units": [{
|
||
"@type": "OrgUnit",
|
||
"name": "Skynet",
|
||
"sortAs": "SKY"
|
||
}, {
|
||
"@type": "OrgUnit",
|
||
"name": "Cybernics"
|
||
}
|
||
],
|
||
"contexts": {
|
||
"work": true
|
||
}
|
||
}`, Organization{
|
||
Type: OrganizationType,
|
||
Name: "Cyberdyne",
|
||
SortAs: "CYBER",
|
||
Units: []OrgUnit{
|
||
{
|
||
Type: OrgUnitType,
|
||
Name: "Skynet",
|
||
SortAs: "SKY",
|
||
},
|
||
{
|
||
Type: OrgUnitType,
|
||
Name: "Cybernics",
|
||
},
|
||
},
|
||
Contexts: map[OrganizationContext]bool{
|
||
OrganizationContextWork: true,
|
||
},
|
||
})
|
||
}
|
||
|
||
func TestPronouns(t *testing.T) {
|
||
jsoneq(t, `{
|
||
"@type": "Pronouns",
|
||
"pronouns": "they/them",
|
||
"contexts": {
|
||
"work": true,
|
||
"private": true
|
||
},
|
||
"pref": 1
|
||
}`, Pronouns{
|
||
Type: PronounsType,
|
||
Pronouns: "they/them",
|
||
Contexts: map[PronounsContext]bool{
|
||
PronounsContextWork: true,
|
||
PronounsContextPrivate: true,
|
||
},
|
||
Pref: 1,
|
||
})
|
||
}
|
||
|
||
func TestTitle(t *testing.T) {
|
||
jsoneq(t, `{
|
||
"@type": "Title",
|
||
"name": "Doctor",
|
||
"kind": "title",
|
||
"organizationId": "407e1992-9a2b-4e4f-a11b-85a509a4b5ae"
|
||
}`, Title{
|
||
Type: TitleType,
|
||
Name: "Doctor",
|
||
Kind: TitleKindTitle,
|
||
OrganizationId: "407e1992-9a2b-4e4f-a11b-85a509a4b5ae",
|
||
})
|
||
}
|
||
|
||
func TestSpeakToAs(t *testing.T) {
|
||
jsoneq(t, `{
|
||
"@type": "SpeakToAs",
|
||
"grammaticalGender": "neuter",
|
||
"pronouns": {
|
||
"a": {
|
||
"@type": "Pronouns",
|
||
"pronouns": "they/them",
|
||
"contexts": {
|
||
"private": true
|
||
},
|
||
"pref": 1
|
||
},
|
||
"b": {
|
||
"@type": "Pronouns",
|
||
"pronouns": "he/him",
|
||
"contexts": {
|
||
"work": true
|
||
},
|
||
"pref": 99
|
||
}
|
||
}
|
||
}`, SpeakToAs{
|
||
Type: SpeakToAsType,
|
||
GrammaticalGender: GrammaticalGenderNeuter,
|
||
Pronouns: map[string]Pronouns{
|
||
"a": {
|
||
Type: PronounsType,
|
||
Pronouns: "they/them",
|
||
Contexts: map[PronounsContext]bool{
|
||
PronounsContextPrivate: true,
|
||
},
|
||
Pref: 1,
|
||
},
|
||
"b": {
|
||
Type: PronounsType,
|
||
Pronouns: "he/him",
|
||
Contexts: map[PronounsContext]bool{
|
||
PronounsContextWork: true,
|
||
},
|
||
Pref: 99,
|
||
},
|
||
},
|
||
})
|
||
}
|
||
|
||
func TestName(t *testing.T) {
|
||
jsoneq(t, `{
|
||
"@type": "Name",
|
||
"components": [
|
||
{ "@type": "NameComponent", "kind": "given", "value": "Diego", "phonetic": "/di\u02C8e\u026A\u0261əʊ/" },
|
||
{ "kind": "surname", "value": "Rivera" },
|
||
{ "kind": "surname2", "value": "Barrientos" }
|
||
],
|
||
"isOrdered": true,
|
||
"defaultSeparator": " ",
|
||
"full": "Diego Rivera Barrientos",
|
||
"sortAs": {
|
||
"surname": "Rivera Barrientos",
|
||
"given": "Diego"
|
||
}
|
||
}`, Name{
|
||
Type: NameType,
|
||
Components: []NameComponent{
|
||
{
|
||
Type: NameComponentType,
|
||
Value: "Diego",
|
||
Kind: NameComponentKindGiven,
|
||
Phonetic: "/diˈeɪɡəʊ/",
|
||
},
|
||
{
|
||
Value: "Rivera",
|
||
Kind: NameComponentKindSurname,
|
||
},
|
||
{
|
||
Value: "Barrientos",
|
||
Kind: NameComponentKindSurname2,
|
||
},
|
||
},
|
||
IsOrdered: true,
|
||
DefaultSeparator: " ",
|
||
Full: "Diego Rivera Barrientos",
|
||
SortAs: map[string]string{
|
||
string(NameComponentKindSurname): "Rivera Barrientos",
|
||
string(NameComponentKindGiven): "Diego",
|
||
},
|
||
})
|
||
}
|
||
|
||
func TestEmailAddress(t *testing.T) {
|
||
jsoneq(t, `{
|
||
"@type": "EmailAddress",
|
||
"address": "camina@opa.org",
|
||
"contexts": {
|
||
"work": true,
|
||
"private": true
|
||
},
|
||
"pref": 1,
|
||
"label": "bosmang"
|
||
}`, EmailAddress{
|
||
Type: EmailAddressType,
|
||
Address: "camina@opa.org",
|
||
Contexts: map[EmailAddressContext]bool{
|
||
EmailAddressContextWork: true,
|
||
EmailAddressContextPrivate: true,
|
||
},
|
||
Pref: 1,
|
||
Label: "bosmang",
|
||
})
|
||
}
|
||
|
||
func TestOnlineService(t *testing.T) {
|
||
jsoneq(t, `{
|
||
"@type": "OnlineService",
|
||
"service": "OPA Network",
|
||
"contexts": {
|
||
"work": true
|
||
},
|
||
"uri": "https://opa.org/cdrummer",
|
||
"user": "cdrummer@opa.org",
|
||
"pref": 12,
|
||
"label": "opa"
|
||
}`, OnlineService{
|
||
Type: OnlineServiceType,
|
||
Service: "OPA Network",
|
||
Contexts: map[OnlineServiceContext]bool{
|
||
OnlineServiceContextWork: true,
|
||
},
|
||
Uri: "https://opa.org/cdrummer", //NOSONAR
|
||
User: "cdrummer@opa.org",
|
||
Pref: 12,
|
||
Label: "opa",
|
||
})
|
||
}
|
||
|
||
func TestPhone(t *testing.T) {
|
||
jsoneq(t, `{
|
||
"@type": "Phone",
|
||
"number": "+15551234567",
|
||
"features": {
|
||
"text": true,
|
||
"main-number": true,
|
||
"cell": true,
|
||
"video": true,
|
||
"voice": true
|
||
},
|
||
"contexts": {
|
||
"work": true,
|
||
"private": true
|
||
},
|
||
"pref": 42,
|
||
"label": "opa"
|
||
}`, Phone{
|
||
Type: PhoneType,
|
||
Number: "+15551234567",
|
||
Features: map[PhoneFeature]bool{
|
||
PhoneFeatureText: true,
|
||
PhoneFeatureMainNumber: true,
|
||
PhoneFeatureMobile: true,
|
||
PhoneFeatureVideo: true,
|
||
PhoneFeatureVoice: true,
|
||
},
|
||
Contexts: map[PhoneContext]bool{
|
||
PhoneContextWork: true,
|
||
PhoneContextPrivate: true,
|
||
},
|
||
Pref: 42,
|
||
Label: "opa",
|
||
})
|
||
}
|
||
|
||
func TestLanguagePref(t *testing.T) {
|
||
jsoneq(t, `{
|
||
"@type": "LanguagePref",
|
||
"language": "fr-BE",
|
||
"contexts": {
|
||
"private": true
|
||
},
|
||
"pref": 2
|
||
}`, LanguagePref{
|
||
Type: LanguagePrefType,
|
||
Language: "fr-BE",
|
||
Contexts: map[LanguagePrefContext]bool{
|
||
LanguagePrefContextPrivate: true,
|
||
},
|
||
Pref: 2,
|
||
})
|
||
}
|
||
|
||
func TestSchedulingAddress(t *testing.T) {
|
||
jsoneq(t, `{
|
||
"@type": "SchedulingAddress",
|
||
"uri": "mailto:camina@opa.org",
|
||
"contexts": {
|
||
"work": true
|
||
},
|
||
"pref": 3,
|
||
"label": "opa"
|
||
}`, SchedulingAddress{
|
||
Type: SchedulingAddressType,
|
||
Uri: "mailto:camina@opa.org",
|
||
Label: "opa",
|
||
Contexts: map[SchedulingAddressContext]bool{
|
||
SchedulingAddressContextWork: true,
|
||
},
|
||
Pref: 3,
|
||
})
|
||
}
|
||
|
||
func TestAddressComponent(t *testing.T) {
|
||
jsoneq(t, `{
|
||
"@type": "AddressComponent",
|
||
"kind": "postcode",
|
||
"value": "12345",
|
||
"phonetic": "un-deux-trois-quatre-cinq"
|
||
}`, AddressComponent{
|
||
Type: AddressComponentType,
|
||
Kind: AddressComponentKindPostcode,
|
||
Value: "12345",
|
||
Phonetic: "un-deux-trois-quatre-cinq",
|
||
})
|
||
}
|
||
|
||
func TestAddress(t *testing.T) {
|
||
jsoneq(t, `{
|
||
"@type": "Address",
|
||
"contexts": {
|
||
"delivery": true,
|
||
"work": true
|
||
},
|
||
"components": [
|
||
{"@type": "AddressComponent", "kind": "number", "value": "54321"},
|
||
{"kind": "separator", "value": " "},
|
||
{"kind": "name", "value": "Oak St"},
|
||
{"kind": "locality", "value": "Reston"},
|
||
{"kind": "region", "value": "VA"},
|
||
{"kind": "separator", "value": " "},
|
||
{"kind": "postcode", "value": "20190"},
|
||
{"kind": "country", "value": "USA"}
|
||
],
|
||
"countryCode": "US",
|
||
"defaultSeparator": ", ",
|
||
"isOrdered": true
|
||
}`, Address{
|
||
Type: AddressType,
|
||
Contexts: map[AddressContext]bool{
|
||
AddressContextDelivery: true,
|
||
AddressContextWork: true,
|
||
},
|
||
Components: []AddressComponent{
|
||
{Type: AddressComponentType, Kind: AddressComponentKindNumber, Value: "54321"},
|
||
{Kind: AddressComponentKindSeparator, Value: " "},
|
||
{Kind: AddressComponentKindName, Value: "Oak St"},
|
||
{Kind: AddressComponentKindLocality, Value: "Reston"},
|
||
{Kind: AddressComponentKindRegion, Value: "VA"},
|
||
{Kind: AddressComponentKindSeparator, Value: " "},
|
||
{Kind: AddressComponentKindPostcode, Value: "20190"},
|
||
{Kind: AddressComponentKindCountry, Value: "USA"},
|
||
},
|
||
CountryCode: "US",
|
||
DefaultSeparator: ", ",
|
||
IsOrdered: true,
|
||
})
|
||
}
|
||
|
||
func TestPartialDate(t *testing.T) {
|
||
jsoneq(t, `{
|
||
"@type": "PartialDate",
|
||
"year": 2025,
|
||
"month": 9,
|
||
"day": 25,
|
||
"calendarScale": "iso8601"
|
||
}`, PartialDate{
|
||
Type: PartialDateType,
|
||
Year: 2025,
|
||
Month: 9,
|
||
Day: 25,
|
||
CalendarScale: "iso8601",
|
||
})
|
||
}
|
||
|
||
func TestTimestamp(t *testing.T) {
|
||
ts, err := time.Parse(time.RFC3339, "2025-09-25T18:26:14.094725532+02:00") //NOSONAR
|
||
require.NoError(t, err)
|
||
jsoneq(t, `{
|
||
"@type": "Timestamp",
|
||
"utc": "2025-09-25T18:26:14.094725532+02:00"
|
||
}`, &Timestamp{
|
||
Type: TimestampType,
|
||
Utc: ts,
|
||
})
|
||
}
|
||
|
||
func TestAnniversaryWithPartialDate(t *testing.T) {
|
||
jsoneq(t, `{
|
||
"@type": "Anniversary",
|
||
"kind": "birth",
|
||
"date": {
|
||
"@type": "PartialDate",
|
||
"year": 2025,
|
||
"month": 9,
|
||
"day": 25
|
||
}
|
||
}`, Anniversary{
|
||
Type: AnniversaryType,
|
||
Kind: AnniversaryKindBirth,
|
||
Date: &PartialDate{
|
||
Type: PartialDateType,
|
||
Year: 2025,
|
||
Month: 9,
|
||
Day: 25,
|
||
},
|
||
})
|
||
}
|
||
|
||
func TestAnniversaryWithTimestamp(t *testing.T) {
|
||
ts, err := time.Parse(time.RFC3339, "2025-09-25T18:26:14.094725532+02:00")
|
||
require.NoError(t, err)
|
||
|
||
jsoneq(t, `{
|
||
"@type": "Anniversary",
|
||
"kind": "birth",
|
||
"date": {
|
||
"@type": "Timestamp",
|
||
"utc": "2025-09-25T18:26:14.094725532+02:00"
|
||
}
|
||
}`, Anniversary{
|
||
Type: AnniversaryType,
|
||
Kind: AnniversaryKindBirth,
|
||
Date: &Timestamp{
|
||
Type: TimestampType,
|
||
Utc: ts,
|
||
},
|
||
})
|
||
}
|
||
|
||
func TestAuthor(t *testing.T) {
|
||
jsoneq(t, `{
|
||
"@type": "Author",
|
||
"name": "Camina Drummer",
|
||
"uri": "https://opa.org/cdrummer"
|
||
}`, Author{
|
||
Type: AuthorType,
|
||
Name: "Camina Drummer",
|
||
Uri: "https://opa.org/cdrummer",
|
||
})
|
||
}
|
||
|
||
func TestNote(t *testing.T) {
|
||
ts, err := time.Parse(time.RFC3339, "2025-09-25T18:26:14.094725532+02:00")
|
||
require.NoError(t, err)
|
||
|
||
jsoneq(t, `{
|
||
"@type": "Note",
|
||
"note": "this is a note",
|
||
"created": "2025-09-25T18:26:14.094725532+02:00",
|
||
"author": {
|
||
"@type": "Author",
|
||
"name": "Camina Drummer",
|
||
"uri": "https://opa.org/cdrummer"
|
||
}
|
||
}`, Note{
|
||
Type: NoteType,
|
||
Note: "this is a note",
|
||
Created: ts,
|
||
Author: &Author{
|
||
Type: AuthorType,
|
||
Name: "Camina Drummer",
|
||
Uri: "https://opa.org/cdrummer",
|
||
},
|
||
})
|
||
}
|
||
|
||
func TestPersonalInfo(t *testing.T) {
|
||
jsoneq(t, `{
|
||
"@type": "PersonalInfo",
|
||
"kind": "expertise",
|
||
"value": "motivation",
|
||
"level": "high",
|
||
"listAs": 1,
|
||
"label": "opa"
|
||
}`, PersonalInfo{
|
||
Type: PersonalInfoType,
|
||
Kind: PersonalInfoKindExpertise,
|
||
Value: "motivation",
|
||
Level: PersonalInfoLevelHigh,
|
||
ListAs: 1,
|
||
Label: "opa",
|
||
})
|
||
}
|